diff --git a/akonadi/collectionmodifyjob.cpp b/akonadi/collectionmodifyjob.cpp index 848076355..3bee16c38 100644 --- a/akonadi/collectionmodifyjob.cpp +++ b/akonadi/collectionmodifyjob.cpp @@ -1,92 +1,98 @@ /* Copyright (c) 2006 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "collectionmodifyjob.h" #include "imapparser_p.h" #include "job_p.h" #include "protocolhelper_p.h" #include "collectionstatistics.h" #include "collection_p.h" using namespace Akonadi; class Akonadi::CollectionModifyJobPrivate : public JobPrivate { public: CollectionModifyJobPrivate( CollectionModifyJob *parent ) : JobPrivate( parent ) { } Collection mCollection; }; CollectionModifyJob::CollectionModifyJob( const Collection &collection, QObject * parent ) : Job( new CollectionModifyJobPrivate( this ), parent ) { Q_D( CollectionModifyJob ); d->mCollection = collection; } CollectionModifyJob::~CollectionModifyJob() { } void CollectionModifyJob::doStart() { Q_D( CollectionModifyJob ); if ( !d->mCollection.isValid() && d->mCollection.remoteId().isEmpty() ) { setError( Unknown ); setErrorText( QLatin1String( "Invalid collection" ) ); emitResult(); return; } QByteArray command = d->newTag(); if ( d->mCollection.isValid() ) command += " MODIFY " + QByteArray::number( d->mCollection.id() ); else command += " RID MODIFY " + ImapParser::quote( d->mCollection.remoteId().toUtf8() ); QByteArray changes; if ( d->mCollection.d_func()->contentTypesChanged ) { QList bList; foreach( const QString &s, d->mCollection.contentMimeTypes() ) bList << s.toLatin1(); changes += " MIMETYPE (" + ImapParser::join( bList, " " ) + ')'; } if ( d->mCollection.parentCollection().id() >= 0 ) changes += " PARENT " + QByteArray::number( d->mCollection.parentCollection().id() ); if ( !d->mCollection.name().isEmpty() ) changes += " NAME " + ImapParser::quote( d->mCollection.name().toUtf8() ); if ( !d->mCollection.remoteId().isNull() ) changes += " REMOTEID " + ImapParser::quote( d->mCollection.remoteId().toUtf8() ); if ( d->mCollection.d_func()->cachePolicyChanged ) changes += ' ' + ProtocolHelper::cachePolicyToByteArray( d->mCollection.cachePolicy() ); if ( d->mCollection.attributes().count() > 0 ) changes += ' ' + ProtocolHelper::attributesToByteArray( d->mCollection ); foreach ( const QByteArray &b, d->mCollection.d_func()->mDeletedAttributes ) changes += " -" + b; if ( changes.isEmpty() ) { emitResult(); return; } command += changes + '\n'; d->writeData( command ); } +Collection CollectionModifyJob::collection() const +{ + const Q_D( CollectionModifyJob ); + return d->mCollection; +} + #include "collectionmodifyjob.moc" diff --git a/akonadi/collectionmodifyjob.h b/akonadi/collectionmodifyjob.h index e05b75415..69a36a3b7 100644 --- a/akonadi/collectionmodifyjob.h +++ b/akonadi/collectionmodifyjob.h @@ -1,80 +1,86 @@ /* Copyright (c) 2006 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_COLLECTIONMODIFYJOB_H #define AKONADI_COLLECTIONMODIFYJOB_H #include "akonadi_export.h" #include namespace Akonadi { class CachePolicy; class Collection; class CollectionModifyJobPrivate; /** * @short Job that modifies a collection in the Akonadi storage. * * This job modifies the properties of an existing collection. * * @code * * Akonadi::Collection collection = ... * * Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob( collection ); * connect( job, SIGNAL( result( KJob* ) ), this, SLOT( modifyResult( KJob* ) ) ); * * @endcode * * @author Volker Krause */ class AKONADI_EXPORT CollectionModifyJob : public Job { Q_OBJECT public: /** * Creates a new collection modify job for the given collection. The collection can be * identified either by its unique identifier or its remote identifier. Since the remote * identifier is not necessarily globally unique, identification by remote identifier only * works inside a resource context (that is from within ResourceBase) and is therefore * limited to one resource. * * @param collection The collection to modify. * @param parent The parent object. */ explicit CollectionModifyJob( const Collection &collection, QObject *parent = 0 ); /** * Destroys the collection modify job. */ ~CollectionModifyJob(); + /** + Returns the modified collection. + @since 4.4 + */ + Collection collection() const; + protected: virtual void doStart(); private: Q_DECLARE_PRIVATE( CollectionModifyJob ) }; } #endif diff --git a/akonadi/collectionsync.cpp b/akonadi/collectionsync.cpp index d9155f92d..db9c8b6d3 100644 --- a/akonadi/collectionsync.cpp +++ b/akonadi/collectionsync.cpp @@ -1,559 +1,561 @@ /* Copyright (c) 2007, 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "collectionsync_p.h" #include "collection.h" #include "collectioncreatejob.h" #include "collectiondeletejob.h" #include "collectionfetchjob.h" #include "collectionmodifyjob.h" #include "collectionfetchscope.h" #include "collectionmovejob.h" #include #include using namespace Akonadi; struct RemoteNode; /** LocalNode is used to build a tree structure of all our locally existing collections. */ struct LocalNode { LocalNode( const Collection &col ) : collection( col ), processed( false ) {} ~LocalNode() { qDeleteAll( childNodes ); qDeleteAll( pendingRemoteNodes ); } Collection collection; QList childNodes; QHash childRidMap; /** When using hierarchical RIDs we attach a list of not yet processable remote nodes to the closest already existing local ancestor node. They will be re-evaluated once a new child node is added. */ QList pendingRemoteNodes; bool processed; }; Q_DECLARE_METATYPE( LocalNode* ) static const char LOCAL_NODE[] = "LocalNode"; /** RemoteNode is used as a container for remote collections which typically don't have a UID set and thus cannot easily be compared or put into maps etc. */ struct RemoteNode { RemoteNode( const Collection &col ) : collection( col ) {} Collection collection; }; Q_DECLARE_METATYPE( RemoteNode* ) static const char REMOTE_NODE[] = "RemoteNode"; /** * @internal */ class CollectionSync::Private { public: Private( CollectionSync *parent ) : q( parent ), pendingJobs( 0 ), progress( 0 ), incremental( false ), streaming( false ), hierarchicalRIDs( false ), localListDone( false ), deliveryDone( false ) { localRoot = new LocalNode( Collection::root() ); localRoot->processed = true; // never try to delete that one localUidMap.insert( localRoot->collection.id(), localRoot ); if ( !hierarchicalRIDs ) localRidMap.insert( QString(), localRoot ); } ~Private() { delete localRoot; } /** Create a local node from the given local collection and integrate it into the local tree structure. */ LocalNode* createLocalNode( const Collection &col ) { + if ( col.remoteId().isEmpty() ) // no remote id here means it hasn't been added to the resource yet, so we exclude it from the sync + return 0; LocalNode *node = new LocalNode( col ); Q_ASSERT( !localUidMap.contains( col.id() ) ); localUidMap.insert( node->collection.id(), node ); if ( !hierarchicalRIDs ) localRidMap.insert( node->collection.remoteId(), node ); // add already existing children if ( localPendingCollections.contains( col.id() ) ) { QList childIds = localPendingCollections.take( col.id() ); foreach ( Collection::Id childId, childIds ) { Q_ASSERT( localUidMap.contains( childId ) ); LocalNode *childNode = localUidMap.value( childId ); node->childNodes.append( childNode ); node->childRidMap.insert( childNode->collection.remoteId(), childNode ); } } // set our parent and add ourselves as child if ( localUidMap.contains( col.parentCollection().id() ) ) { LocalNode* parentNode = localUidMap.value( col.parentCollection().id() ); parentNode->childNodes.append( node ); parentNode->childRidMap.insert( node->collection.remoteId(), node ); } else { localPendingCollections[ col.parentCollection().id() ].append( col.id() ); } return node; } /** Same as createLocalNode() for remote collections. */ void createRemoteNode( const Collection &col ) { if ( col.remoteId().isEmpty() ) { kWarning() << "Collection '" << col.name() << "' does not have a remote identifier - skipping"; return; } RemoteNode *node = new RemoteNode( col ); localRoot->pendingRemoteNodes.append( node ); } /** Create local nodes as we receive the local listing from the Akonadi server. */ void localCollectionsReceived( const Akonadi::Collection::List &localCols ) { foreach ( const Collection &c, localCols ) createLocalNode( c ); } /** Once the local collection listing finished we can continue with the interesting stuff. */ void localCollectionFetchResult( KJob *job ) { if ( job->error() ) return; // handled by the base class // safety check: the local tree has to be connected if ( !localPendingCollections.isEmpty() ) { q->setError( Unknown ); q->setErrorText( QLatin1String( "Inconsistent local collection tree detected! OMG, what have you done to my database?!?!" ) ); q->emitResult(); return; } localListDone = true; execute(); } /** Find the local node that matches the given remote collection, returns 0 if that doesn't exist (yet). */ LocalNode* findMatchingLocalNode( const Collection &collection ) { if ( !hierarchicalRIDs ) { if ( localRidMap.contains( collection.remoteId() ) ) return localRidMap.value( collection.remoteId() ); return 0; } else { if ( collection.id() == Collection::root().id() || collection.remoteId() == Collection::root().remoteId() ) return localRoot; LocalNode *localParent = 0; if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { kWarning() << "Remote collection without valid parent found: " << collection; return 0; } if ( collection.parentCollection().id() == Collection::root().id() || collection.parentCollection().remoteId() == Collection::root().remoteId() ) localParent = localRoot; else localParent = findMatchingLocalNode( collection.parentCollection() ); if ( localParent && localParent->childRidMap.contains( collection.remoteId() ) ) return localParent->childRidMap.value( collection.remoteId() ); return 0; } } /** Find the local node that is the nearest ancestor of the given remote collection (when using hierarchical RIDs only, otherwise it's always the local root node). Never returns 0. */ LocalNode* findBestLocalAncestor( const Collection &collection, bool *exactMatch = 0 ) { if ( !hierarchicalRIDs ) return localRoot; + if ( collection == Collection::root() ) { + if ( exactMatch ) *exactMatch = true; + return localRoot; + } if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { kWarning() << "Remote collection without valid parent found: " << collection; return 0; } - if ( collection.parentCollection() == Collection::root() ) { - if ( exactMatch ) *exactMatch = true; - return localRoot; - } bool parentIsExact = false; LocalNode *localParent = findBestLocalAncestor( collection.parentCollection(), &parentIsExact ); if ( !parentIsExact ) { if ( exactMatch ) *exactMatch = false; return localParent; } if ( localParent->childRidMap.contains( collection.remoteId() ) ) { if ( exactMatch ) *exactMatch = true; return localParent->childRidMap.value( collection.remoteId() ); } if ( exactMatch ) *exactMatch = false; return localParent; } /** Checks the pending remote nodes attached to the given local root node to see if any of them can be processed by now. If not, they are moved to the closest ancestor available. */ void processPendingRemoteNodes( LocalNode *localRoot ) { QList pendingRemoteNodes( localRoot->pendingRemoteNodes ); localRoot->pendingRemoteNodes.clear(); QHash > pendingCreations; foreach ( RemoteNode *remoteNode, pendingRemoteNodes ) { // step 1: see if we have a matching local node already LocalNode *localNode = findMatchingLocalNode( remoteNode->collection ); if ( localNode ) { Q_ASSERT( !localNode->processed ); updateLocalCollection( localNode, remoteNode ); continue; } // step 2: check if we have the parent at least, then we can create it localNode = findMatchingLocalNode( remoteNode->collection.parentCollection() ); if ( localNode ) { pendingCreations[localNode].append( remoteNode ); continue; } // step 3: find the best matching ancestor and enqueue it for later processing localNode = findBestLocalAncestor( remoteNode->collection ); if ( !localNode ) { q->setError( Unknown ); q->setErrorText( QLatin1String( "Remote collection without root-terminated ancestor chain provided, fix your resource dude!" ) ); q->emitResult(); return; } localNode->pendingRemoteNodes.append( remoteNode ); } // process the now possible collection creations for ( QHash >::const_iterator it = pendingCreations.constBegin(); it != pendingCreations.constEnd(); ++it ) { createLocalCollections( it.key(), it.value() ); } } /** Performs a local update for the given node pair. */ void updateLocalCollection( LocalNode *localNode, RemoteNode *remoteNode ) { ++pendingJobs; Collection upd( remoteNode->collection ); upd.setId( localNode->collection.id() ); CollectionModifyJob *mod = new CollectionModifyJob( upd, q ); connect( mod, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*)) ); // detecting moves is only possible with global RIDs if ( !hierarchicalRIDs ) { LocalNode *oldParent = localUidMap.value( localNode->collection.parentCollection().id() ); LocalNode *newParent = findMatchingLocalNode( remoteNode->collection.parentCollection() ); if ( oldParent != newParent ) { ++pendingJobs; CollectionMoveJob *move = new CollectionMoveJob( upd, newParent->collection, q ); connect( move, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*)) ); } } localNode->processed = true; delete remoteNode; } void updateLocalCollectionResult( KJob* job ) { --pendingJobs; if ( job->error() ) return; // handled by the base class if ( qobject_cast( job ) ) ++progress; checkDone(); } /** Creates local folders for the given local parent and remote nodes. @todo group CollectionCreateJobs into a single one once it supports that */ void createLocalCollections( LocalNode* localParent, QList remoteNodes ) { foreach ( RemoteNode *remoteNode, remoteNodes ) { ++pendingJobs; Collection col( remoteNode->collection ); col.setParentCollection( localParent->collection ); CollectionCreateJob *create = new CollectionCreateJob( col, q ); create->setProperty( LOCAL_NODE, QVariant::fromValue( localParent ) ); create->setProperty( REMOTE_NODE, QVariant::fromValue( remoteNode ) ); connect( create, SIGNAL(result(KJob*)), q, SLOT(createLocalCollectionResult(KJob*)) ); } } void createLocalCollectionResult( KJob* job ) { --pendingJobs; if ( job->error() ) return; // handled by the base class const Collection newLocal = static_cast( job )->collection(); LocalNode* localNode = createLocalNode( newLocal ); localNode->processed = true; LocalNode* localParent = job->property( LOCAL_NODE ).value(); Q_ASSERT( localParent->childNodes.contains( localNode ) ); RemoteNode* remoteNode = job->property( REMOTE_NODE ).value(); delete remoteNode; ++progress; processPendingRemoteNodes( localParent ); if ( !hierarchicalRIDs ) processPendingRemoteNodes( localRoot ); checkDone(); } /** Find all local nodes that are not marked as processed. */ Collection::List findUnprocessedLocalCollections( LocalNode *localNode ) { Collection::List rv; if ( !localNode->processed ) { rv.append( localNode->collection ); return rv; } foreach ( LocalNode *child, localNode->childNodes ) rv.append( findUnprocessedLocalCollections( child ) ); return rv; } /** Deletes unprocessed local nodes, in non-incremental mode. */ void deleteUnprocessedLocalNodes() { if ( incremental ) return; Collection::List cols = findUnprocessedLocalCollections( localRoot ); deleteLocalCollections( cols ); } /** Deletes the given collection list. @todo optimite delete job to support batch operations */ void deleteLocalCollections( const Collection::List &cols ) { q->setTotalAmount( KJob::Bytes, q->totalAmount( KJob::Bytes ) + cols.size() ); foreach ( const Collection &col, cols ) { ++pendingJobs; CollectionDeleteJob *job = new CollectionDeleteJob( col, q ); connect( job, SIGNAL(result(KJob*)), q, SLOT(deleteLocalCollectionsResult(KJob*)) ); } } void deleteLocalCollectionsResult( KJob *job ) { --pendingJobs; if ( job->error() ) return; // handled by the base class ++progress; checkDone(); } /** Process what's currently available. */ void execute() { if ( !localListDone ) return; processPendingRemoteNodes( localRoot ); if ( !incremental && deliveryDone ) deleteUnprocessedLocalNodes(); if ( !hierarchicalRIDs ) { deleteLocalCollections( removedRemoteCollections ); } else { Collection::List localCols; foreach ( const Collection &c, removedRemoteCollections ) { LocalNode *node = findMatchingLocalNode( c ); if ( node ) localCols.append( node->collection ); } deleteLocalCollections( localCols ); } removedRemoteCollections.clear(); checkDone(); } /** Finds pending remote nodes, which at the end of the day should be an empty set. */ QList findPendingRemoteNodes( LocalNode *localNode ) { QList rv; rv.append( localNode->pendingRemoteNodes ); foreach ( LocalNode *child, localNode->childNodes ) rv.append( findPendingRemoteNodes( child ) ); return rv; } /** Are we there yet?? @todo progress reporting */ void checkDone() { q->setProcessedAmount( KJob::Bytes, progress ); // still running jobs or not fully delivered local/remote state if ( !deliveryDone || pendingJobs > 0 || !localListDone ) return; // safety check: there must be no pending remote nodes anymore QList orphans = findPendingRemoteNodes( localRoot ); if ( !orphans.isEmpty() ) { q->setError( Unknown ); q->setErrorText( QLatin1String( "Found unresolved orphan collections" ) ); foreach ( RemoteNode* orphan, orphans ) kDebug() << "found orphan collection:" << orphan->collection; q->emitResult(); return; } q->commit(); } CollectionSync *q; QString resourceId; int pendingJobs; int progress; LocalNode* localRoot; QHash localUidMap; QHash localRidMap; // temporary during build-up of the local node tree, must be empty afterwards QHash > localPendingCollections; // removed remote collections in incremental mode Collection::List removedRemoteCollections; bool incremental; bool streaming; bool hierarchicalRIDs; bool localListDone; bool deliveryDone; }; CollectionSync::CollectionSync( const QString &resourceId, QObject *parent ) : TransactionSequence( parent ), d( new Private( this ) ) { d->resourceId = resourceId; setTotalAmount( KJob::Bytes, 0 ); } CollectionSync::~CollectionSync() { delete d; } void CollectionSync::setRemoteCollections(const Collection::List & remoteCollections) { setTotalAmount( KJob::Bytes, totalAmount( KJob::Bytes ) + remoteCollections.count() ); foreach ( const Collection &c, remoteCollections ) d->createRemoteNode( c ); if ( !d->streaming ) d->deliveryDone = true; d->execute(); } void CollectionSync::setRemoteCollections(const Collection::List & changedCollections, const Collection::List & removedCollections) { setTotalAmount( KJob::Bytes, totalAmount( KJob::Bytes ) + changedCollections.count() ); d->incremental = true; foreach ( const Collection &c, changedCollections ) d->createRemoteNode( c ); d->removedRemoteCollections += removedCollections; if ( !d->streaming ) d->deliveryDone = true; d->execute(); } void CollectionSync::doStart() { CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, this ); job->fetchScope().setResource( d->resourceId ); connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(localCollectionsReceived(Akonadi::Collection::List)) ); connect( job, SIGNAL(result(KJob*)), SLOT(localCollectionFetchResult(KJob*)) ); } void CollectionSync::setStreamingEnabled( bool streaming ) { d->streaming = streaming; } void CollectionSync::retrievalDone() { d->deliveryDone = true; d->execute(); } void CollectionSync::setHierarchicalRemoteIds( bool hierarchical ) { d->hierarchicalRIDs = hierarchical; } #include "collectionsync_p.moc" diff --git a/akonadi/entitycache_p.h b/akonadi/entitycache_p.h index 61772bd79..5b00ecdd9 100644 --- a/akonadi/entitycache_p.h +++ b/akonadi/entitycache_p.h @@ -1,232 +1,233 @@ /* Copyright (c) 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_ENTITYCACHE_P_H #define AKONADI_ENTITYCACHE_P_H #include #include #include #include #include #include #include #include #include #include class KJob; namespace Akonadi { /** @internal QObject part of EntityCache. */ class EntityCacheBase : public QObject { Q_OBJECT public: explicit EntityCacheBase (QObject * parent = 0); signals: void dataAvailable(); private slots: virtual void fetchResult( KJob* job ) = 0; }; template struct EntityCacheNode { EntityCacheNode() : pending( false ), invalid( false ) {} EntityCacheNode( typename T::Id id ) : entity( T( id ) ), pending( true ), invalid( false ) {} T entity; bool pending; bool invalid; }; /** * @internal * A in-memory FIFO cache for a small amount of Entity objects. */ template class EntityCache : public EntityCacheBase { public: explicit EntityCache( int maxCapacity, QObject *parent = 0 ) : EntityCacheBase( parent ), mCapacity( maxCapacity ) {} ~EntityCache() { qDeleteAll( mCache ); } /** Object is available in the cache and can be retrieved. */ bool isCached( typename T::Id id ) const { EntityCacheNode* node = cacheNodeForId( id ); return node && !node->pending; } /** Object has been requested but is not yet loaded into the cache or is already available. */ bool isRequested( typename T::Id id ) const { return cacheNodeForId( id ); } /** Returns the cached object if available, an empty instance otherwise. */ T retrieve( typename T::Id id ) const { EntityCacheNode* node = cacheNodeForId( id ); if ( node && !node->pending && !node->invalid ) return node->entity; return T(); } /** Marks the cache entry as invalid, use in case the object has been deleted on the server. */ void invalidate( typename T::Id id ) { EntityCacheNode* node = cacheNodeForId( id ); if ( node ) node->invalid = true; } /** Triggers a re-fetching of a cache entry, use if it has changed on the server. */ void update( typename T::Id id, const FetchScope &scope ) { EntityCacheNode* node = cacheNodeForId( id ); if ( node ) { mCache.removeAll( node ); if ( node->pending ) request( id, scope ); delete node; } } /** Requests the object to be cached if it is not yet in the cache. @returns @c true if it was in the cache already. */ bool ensureCached( typename T::Id id, const FetchScope &scope ) { EntityCacheNode* node = cacheNodeForId( id ); if ( !node ) { request( id, scope ); return false; } return !node->pending; } /** Asks the cache to retrieve @p id. @p request is used as a token to indicate which request has been finished in the dataAvailable() signal. */ void request( typename T::Id id, const FetchScope &scope ) { Q_ASSERT( !isRequested( id ) ); shrinkCache(); EntityCacheNode *node = new EntityCacheNode( id ); FetchJob* job = createFetchJob( id ); job->setFetchScope( scope ); job->setProperty( "EntityCacheNode", QVariant::fromValue( id ) ); connect( job, SIGNAL(result(KJob*)), SLOT(fetchResult(KJob*)) ); mCache.enqueue( node ); } private: EntityCacheNode* cacheNodeForId( typename T::Id id ) const { for ( typename QQueue*>::const_iterator it = mCache.constBegin(), endIt = mCache.constEnd(); it != endIt; ++it ) { if ( (*it)->entity.id() == id ) return *it; } return 0; } void fetchResult( KJob* job ) { typename T::Id id = job->property( "EntityCacheNode" ).template value(); EntityCacheNode *node = cacheNodeForId( id ); if ( !node ) return; // got replaced in the meantime node->pending = false; extractResult( node, job ); - if ( node->entity.id() != id ) { // make sure we find this node again if something went wrong here... - kWarning() << "Something went very wrong..."; + // make sure we find this node again if something went wrong here, + // most likely the object got deleted from the server in the meantime + if ( node->entity.id() != id ) { node->entity.setId( id ); node->invalid = true; } emit dataAvailable(); } void extractResult( EntityCacheNode* node, KJob* job ) const; inline FetchJob* createFetchJob( typename T::Id id ) { return new FetchJob( T( id ), this ); } /** Tries to reduce the cache size until at least one more object fits in. */ void shrinkCache() { while ( mCache.size() >= mCapacity && !mCache.first()->pending ) delete mCache.dequeue(); } private: QQueue*> mCache; int mCapacity; }; template<> inline void EntityCache::extractResult( EntityCacheNode* node, KJob *job ) const { CollectionFetchJob* fetch = qobject_cast( job ); Q_ASSERT( fetch ); if ( fetch->collections().isEmpty() ) node->entity = Collection(); else node->entity = fetch->collections().first(); } template<> inline void EntityCache::extractResult( EntityCacheNode* node, KJob *job ) const { ItemFetchJob* fetch = qobject_cast( job ); Q_ASSERT( fetch ); if ( fetch->items().isEmpty() ) node->entity = Item(); else node->entity = fetch->items().first(); } template<> inline CollectionFetchJob* EntityCache::createFetchJob( Collection::Id id ) { return new CollectionFetchJob( Collection( id ), CollectionFetchJob::Base, this ); } typedef EntityCache CollectionCache; typedef EntityCache ItemCache; } #endif diff --git a/akonadi/itemsync.cpp b/akonadi/itemsync.cpp index 895fa25a6..91888a93d 100644 --- a/akonadi/itemsync.cpp +++ b/akonadi/itemsync.cpp @@ -1,397 +1,399 @@ /* Copyright (c) 2007 Tobias Koenig Copyright (c) 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "itemsync.h" #include "collection.h" #include "item.h" #include "itemcreatejob.h" #include "itemdeletejob.h" #include "itemfetchjob.h" #include "itemmodifyjob.h" #include "transactionsequence.h" #include "itemfetchscope.h" #include #include using namespace Akonadi; /** * @internal */ class ItemSync::Private { public: Private( ItemSync *parent ) : q( parent ), mTransactionMode( Single ), mCurrentTransaction( 0 ), mTransactionJobs( 0 ), mPendingJobs( 0 ), mProgress( 0 ), mTotalItems( -1 ), mTotalItemsProcessed( 0 ), mStreaming( false ), mIncremental( false ), mLocalListDone( false ), mDeliveryDone( false ) { // we want to fetch all data by default mFetchScope.fetchFullPayload(); mFetchScope.fetchAllAttributes(); } void createLocalItem( const Item &item ); void checkDone(); void slotLocalListDone( KJob* ); void slotLocalChangeDone( KJob* ); void execute(); void processItems(); void deleteItems( const Item::List &items ); void slotTransactionResult( KJob *job ); Job* subjobParent() const; ItemSync *q; Collection mSyncCollection; QHash mLocalItemsById; QHash mLocalItemsByRemoteId; QSet mUnprocessedLocalItems; // transaction mode, TODO: make this public API? enum TransactionMode { Single, Chunkwise, None }; TransactionMode mTransactionMode; TransactionSequence *mCurrentTransaction; int mTransactionJobs; // fetch scope for initial item listing ItemFetchScope mFetchScope; // remote items Akonadi::Item::List mRemoteItems; // removed remote items Item::List mRemovedRemoteItems; // create counter int mPendingJobs; int mProgress; int mTotalItems; int mTotalItemsProcessed; bool mStreaming; bool mIncremental; bool mLocalListDone; bool mDeliveryDone; }; void ItemSync::Private::createLocalItem( const Item & item ) { mPendingJobs++; ItemCreateJob *create = new ItemCreateJob( item, mSyncCollection, subjobParent() ); q->connect( create, SIGNAL( result( KJob* ) ), q, SLOT( slotLocalChangeDone( KJob* ) ) ); } void ItemSync::Private::checkDone() { q->setProcessedAmount( KJob::Bytes, mProgress ); if ( mPendingJobs > 0 || !mDeliveryDone || mTransactionJobs > 0 ) return; q->emitResult(); } ItemSync::ItemSync( const Collection &collection, QObject *parent ) : Job( parent ), d( new Private( this ) ) { d->mSyncCollection = collection; } ItemSync::~ItemSync() { delete d; } void ItemSync::setFullSyncItems( const Item::List &items ) { Q_ASSERT( !d->mIncremental ); if ( !d->mStreaming ) d->mDeliveryDone = true; d->mRemoteItems += items; d->mTotalItemsProcessed += items.count(); kDebug() << "Received: " << items.count() << "In total: " << d->mTotalItemsProcessed << " Wanted: " << d->mTotalItems; setTotalAmount( KJob::Bytes, d->mTotalItemsProcessed ); if ( d->mTotalItemsProcessed == d->mTotalItems ) d->mDeliveryDone = true; d->execute(); } void ItemSync::setTotalItems( int amount ) { Q_ASSERT( !d->mIncremental ); Q_ASSERT( amount >= 0 ); setStreamingEnabled( true ); kDebug() << amount; d->mTotalItems = amount; setTotalAmount( KJob::Bytes, amount ); if ( d->mTotalItems == 0 ) { d->mDeliveryDone = true; d->execute(); } } void ItemSync::setIncrementalSyncItems( const Item::List &changedItems, const Item::List &removedItems ) { d->mIncremental = true; if ( !d->mStreaming ) d->mDeliveryDone = true; d->mRemoteItems += changedItems; d->mRemovedRemoteItems += removedItems; d->mTotalItemsProcessed += changedItems.count() + removedItems.count(); setTotalAmount( KJob::Bytes, d->mTotalItemsProcessed ); if ( d->mTotalItemsProcessed == d->mTotalItems ) d->mDeliveryDone = true; d->execute(); } void ItemSync::setFetchScope( ItemFetchScope &fetchScope ) { d->mFetchScope = fetchScope; } ItemFetchScope &ItemSync::fetchScope() { return d->mFetchScope; } void ItemSync::doStart() { ItemFetchJob* job = new ItemFetchJob( d->mSyncCollection, this ); job->setFetchScope( d->mFetchScope ); // we only can fetch parts already in the cache, otherwise this will deadlock job->fetchScope().setCacheOnly( true ); connect( job, SIGNAL( result( KJob* ) ), SLOT( slotLocalListDone( KJob* ) ) ); } bool ItemSync::updateItem( const Item &storedItem, Item &newItem ) { /* * We know that this item has changed (as it is part of the * incremental changed list), so we just put it into the * storage. */ if ( d->mIncremental ) return true; // Check whether the flags differ if ( storedItem.flags() != newItem.flags() ) { kDebug() << "Stored flags " << storedItem.flags() << "new flags " << newItem.flags(); return true; } // Check whether the new item contains unknown parts QSet missingParts = storedItem.loadedPayloadParts(); missingParts.subtract( newItem.loadedPayloadParts() ); if ( !missingParts.isEmpty() ) return true; // ### FIXME SLOW!!! // If the available part identifiers don't differ, check // whether the content of the payload differs if ( storedItem.payloadData() != newItem.payloadData() ) return true; // check if remote attributes have been changed foreach ( Attribute* attr, newItem.attributes() ) { if ( !storedItem.hasAttribute( attr->type() ) ) return true; if ( attr->serialized() != storedItem.attribute( attr->type() )->serialized() ) return true; } return false; } void ItemSync::Private::slotLocalListDone( KJob * job ) { if ( job->error() ) return; const Item::List list = static_cast( job )->items(); foreach ( const Item &item, list ) { + if ( item.remoteId().isEmpty() ) + continue; mLocalItemsById.insert( item.id(), item ); mLocalItemsByRemoteId.insert( item.remoteId(), item ); mUnprocessedLocalItems.insert( item ); } mLocalListDone = true; execute(); } void ItemSync::Private::execute() { if ( !mLocalListDone ) return; if ( (mTransactionMode == Single && !mCurrentTransaction) || mTransactionMode == Chunkwise ) { ++mTransactionJobs; mCurrentTransaction = new TransactionSequence( q ); connect( mCurrentTransaction, SIGNAL(result(KJob*)), q, SLOT(slotTransactionResult(KJob*)) ); } processItems(); if ( !mDeliveryDone ) { if ( mTransactionMode == Chunkwise && mCurrentTransaction ) { mCurrentTransaction->commit(); mCurrentTransaction = 0; } return; } // removed if ( !mIncremental ) { mRemovedRemoteItems = mUnprocessedLocalItems.toList(); mUnprocessedLocalItems.clear(); } deleteItems( mRemovedRemoteItems ); mLocalItemsById.clear(); mLocalItemsByRemoteId.clear(); mRemovedRemoteItems.clear(); if ( mCurrentTransaction ) { mCurrentTransaction->commit(); mCurrentTransaction = 0; } checkDone(); } void ItemSync::Private::processItems() { // added / updated foreach ( Item remoteItem, mRemoteItems ) { //krazy:exclude=foreach non-const is needed here #ifndef NDEBUG if ( remoteItem.remoteId().isEmpty() ) { kWarning() << "Item " << remoteItem.id() << " does not have a remote identifier"; } #endif Item localItem = mLocalItemsById.value( remoteItem.id() ); if ( !localItem.isValid() ) localItem = mLocalItemsByRemoteId.value( remoteItem.remoteId() ); mUnprocessedLocalItems.remove( localItem ); // missing locally if ( !localItem.isValid() ) { createLocalItem( remoteItem ); continue; } if ( q->updateItem( localItem, remoteItem ) ) { mPendingJobs++; remoteItem.setId( localItem.id() ); remoteItem.setRevision( localItem.revision() ); remoteItem.setSize( localItem.size() ); remoteItem.setRemoteId( localItem.remoteId() ); // in case someone clears remoteId by accident ItemModifyJob *mod = new ItemModifyJob( remoteItem, subjobParent() ); q->connect( mod, SIGNAL( result( KJob* ) ), q, SLOT( slotLocalChangeDone( KJob* ) ) ); } else { mProgress++; } } mRemoteItems.clear(); } void ItemSync::Private::deleteItems( const Item::List &items ) { foreach ( const Item &item, items ) { Item delItem( item ); if ( !item.isValid() ) { delItem = mLocalItemsByRemoteId.value( item.remoteId() ); } if ( !delItem.isValid() ) { #ifndef NDEBUG kWarning() << "Delete item (remoteeId=" << delItem.remoteId() << "mimeType=" << delItem.mimeType() << ") does not have a valid UID and no item with that remote ID exists either"; #endif continue; } mPendingJobs++; ItemDeleteJob *job = new ItemDeleteJob( delItem, subjobParent() ); q->connect( job, SIGNAL( result( KJob* ) ), q, SLOT( slotLocalChangeDone( KJob* ) ) ); } } void ItemSync::Private::slotLocalChangeDone( KJob * job ) { if ( job->error() ) return; mPendingJobs--; mProgress++; checkDone(); } void ItemSync::Private::slotTransactionResult( KJob *job ) { if ( job->error() ) return; --mTransactionJobs; if ( mCurrentTransaction == job ) mCurrentTransaction = 0; checkDone(); } Job * ItemSync::Private::subjobParent() const { if ( mCurrentTransaction && mTransactionMode != None ) return mCurrentTransaction; return q; } void ItemSync::setStreamingEnabled(bool enable) { d->mStreaming = enable; } void ItemSync::deliveryDone() { Q_ASSERT( d->mStreaming ); d->mDeliveryDone = true; d->execute(); } #include "itemsync.moc" diff --git a/akonadi/monitor.h b/akonadi/monitor.h index 7ef1e0256..f469bb24a 100644 --- a/akonadi/monitor.h +++ b/akonadi/monitor.h @@ -1,377 +1,379 @@ /* Copyright (c) 2006 - 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_MONITOR_H #define AKONADI_MONITOR_H #include #include #include namespace Akonadi { class CollectionFetchScope; class CollectionStatistics; class Item; class ItemFetchScope; class MonitorPrivate; class Session; /** * @short Monitors an item or collection for changes. * * The Monitor emits signals if some of these objects are changed or * removed or new ones are added to the Akonadi storage. * * Optionally, the changed objects can be fetched automatically from the server. * To enable this, see itemFetchScope() and collectionFetchScope(). * * @todo: distinguish between monitoring collection properties and collection content. * @todo: special case for collection content counts changed * * @author Volker Krause */ class AKONADI_EXPORT Monitor : public QObject { Q_OBJECT public: /** * Creates a new monitor. * * @param parent The parent object. */ explicit Monitor( QObject *parent = 0 ); /** * Destroys the monitor. */ virtual ~Monitor(); /** * Sets whether the specified collection shall be monitored for changes. * * @param collection The collection to monitor. * If this collection is Collection::root(), all collections * in the Akonadi storage will be monitored. */ void setCollectionMonitored( const Collection &collection, bool monitored = true ); /** * Sets whether the specified item shall be monitored for changes. * * @param item The item to monitor. */ void setItemMonitored( const Item &item, bool monitored = true ); /** * Sets whether the specified resource shall be monitored for changes. * * @param resource The resource identifier. */ void setResourceMonitored( const QByteArray &resource, bool monitored = true ); /** * Sets whether objects of the specified mime type shall be monitored for changes. * * @param mimetype The mime type to monitor. */ void setMimeTypeMonitored( const QString &mimetype, bool monitored = true ); /** * Sets whether all items shall be monitored. */ void setAllMonitored( bool monitored = true ); /** * Ignores all change notifications caused by the given session. * * @param session The session you want to ignore. */ void ignoreSession( Session *session ); /** * Enables automatic fetching of changed collections from the Akonadi storage. * * @param enable @c true enables automatic fetching, @c false disables automatic fetching. */ void fetchCollection( bool enable ); /** * Enables automatic fetching of changed collection statistics information from * the Akonadi storage. * * @param enable @c true to enables automatic fetching, @c false disables automatic fetching. */ void fetchCollectionStatistics( bool enable ); /** * Sets the item fetch scope. * * Controls how much of an item's data is fetched from the server, e.g. * whether to fetch the full item payload or only meta data. * * @param fetchScope The new scope for item fetch operations. * * @see itemFetchScope() */ void setItemFetchScope( const ItemFetchScope &fetchScope ); /** * Returns the item fetch scope. * * Since this returns a reference it can be used to conveniently modify the * current scope in-place, i.e. by calling a method on the returned reference * without storing it in a local variable. See the ItemFetchScope documentation * for an example. * * @return a reference to the current item fetch scope * * @see setItemFetchScope() for replacing the current item fetch scope */ ItemFetchScope &itemFetchScope(); /** * Sets the collection fetch scope. * * Controls which collections are monitored and how much of a collection's data * is fetched from the server. * * @param fetchScope The new scope for collection fetch operations. * * @see collectionFetchScope() * @since 4.4 */ void setCollectionFetchScope( const CollectionFetchScope &fetchScope ); /** * Returns the collection fetch scope. * * Since this returns a reference it can be used to conveniently modify the * current scope in-place, i.e. by calling a method on the returned reference * without storing it in a local variable. See the CollectionFetchScope documentation * for an example. * * @return a reference to the current collection fetch scope * * @see setCollectionFetchScope() for replacing the current collection fetch scope * @since 4.4 */ CollectionFetchScope &collectionFetchScope(); /** * Returns the list of collections being monitored. * * @since 4.3 */ Collection::List collectionsMonitored() const; /** * Returns the set of items being monitored. * * @since 4.3 */ QList itemsMonitored() const; /** * Returns the set of mimetypes being monitored. * * @since 4.3 */ QStringList mimeTypesMonitored() const; /** * Returns the set of identifiers for resources being monitored. * * @since 4.3 */ QList resourcesMonitored() const; /** * Returns true if everything is being monitored. * * @since 4.3 */ bool isAllMonitored() const; Q_SIGNALS: /** * This signal is emitted if a monitored item has changed, e.g. item parts have been modified. * * @param item The changed item. * @param partIdentifiers The identifiers of the item parts that has been changed. */ void itemChanged( const Akonadi::Item &item, const QSet &partIdentifiers ); /** * This signal is emitted if a monitored item has been moved between two collections * * @param item The moved item. * @param collectionSource The collection the item has been moved from. * @param collectionDestination The collection the item has been moved to. */ void itemMoved( const Akonadi::Item &item, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination ); /** * This signal is emitted if an item has been added to a monitored collection in the Akonadi storage. * * @param item The new item. * @param collection The collection the item has been added to. */ void itemAdded( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * This signal is emitted if * - a monitored item has been removed from the Akonadi storage * or * - a item has been removed from a monitored collection. * * @param item The removed item. */ void itemRemoved( const Akonadi::Item &item ); //TODO remove if we are sure nobody uses it any longer void itemRemoved( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * This signal is emitted if a reference to an item is added to a virtual collection. * @param item The linked item. * @param collection The collection the item is linked to. * * @since 4.2 */ void itemLinked( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * This signal is emitted if a reference to an item is removed from a virtual collection. * @param item The unlinked item. * @param collection The collection the item is unlinked from. * * @since 4.2 */ void itemUnlinked( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * This signal is emitted if a new collection has been added to a monitored collection in the Akonadi storage. * * @param collection The new collection. * @param parent The parent collection. */ void collectionAdded( const Akonadi::Collection &collection, const Akonadi::Collection &parent ); /** * This signal is emitted if a monitored collection has been changed (properties or content). * * @param collection The changed collection. */ void collectionChanged( const Akonadi::Collection &collection ); /** * This signals is emitted if a monitored collection has been moved. * * @param collection The moved collection. * @param source The previous parent collection. * @param distination The new parent collection. */ void collectionMoved( const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &destination ); /** * This signal is emitted if a monitored collection has been removed from the Akonadi storage. * * @param collection The removed collection. */ void collectionRemoved( const Akonadi::Collection &collection ); /** * This signal is emitted if the statistics information of a monitored collection * has changed. * * @param id The collection identifier of the changed collection. * @param statistics The updated collection statistics, invalid if automatic * fetching of statistics changes is disabled. */ void collectionStatisticsChanged( Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistics ); /** * This signal is emitted if the Monitor starts or stops monitoring @p collection explicitly. * @param collection The collection * @param monitored Whether the collection is now being monitored or not. * * @since 4.3 */ void collectionMonitored( const Akonadi::Collection &collection, bool monitored ); /** * This signal is emitted if the Monitor starts or stops monitoring @p item explicitly. * @param item The item * @param monitored Whether the item is now being monitored or not. * * @since 4.3 */ void itemMonitored( const Akonadi::Item &item, bool monitored ); /** * This signal is emitted if the Monitor starts or stops monitoring the resource with the identifier @p identifier explicitly. * @param identifier The identifier of the resource. * @param monitored Whether the resource is now being monitored or not. * * @since 4.3 */ void resourceMonitored( const QByteArray &identifier, bool monitored ); /** * This signal is emitted if the Monitor starts or stops monitoring @p mimeType explicitly. * @param mimeType The mimeType. * @param monitored Whether the mimeType is now being monitored or not. * * @since 4.3 */ void mimeTypeMonitored( const QString &mimeType, bool monitored ); /** * This signal is emitted if the Monitor starts or stops monitoring everything. * @param monitored Whether everything is now being monitored or not. * * @since 4.3 */ void allMonitored( bool monitored ); protected: //@cond PRIVATE MonitorPrivate *d_ptr; explicit Monitor( MonitorPrivate *d, QObject *parent = 0 ); //@endcond private: Q_DECLARE_PRIVATE( Monitor ) //@cond PRIVATE Q_PRIVATE_SLOT( d_ptr, void slotSessionDestroyed( QObject* ) ) Q_PRIVATE_SLOT( d_ptr, void slotStatisticsChangedFinished( KJob* ) ) Q_PRIVATE_SLOT( d_ptr, void slotFlushRecentlyChangedCollections() ) Q_PRIVATE_SLOT( d_ptr, void slotNotify( const Akonadi::NotificationMessage::List& ) ) Q_PRIVATE_SLOT( d_ptr, void dataAvailable() ) + + friend class ResourceBasePrivate; //@endcond }; } #endif diff --git a/akonadi/monitor_p.cpp b/akonadi/monitor_p.cpp index 6042c1f15..8baff38de 100644 --- a/akonadi/monitor_p.cpp +++ b/akonadi/monitor_p.cpp @@ -1,333 +1,338 @@ /* Copyright (c) 2007 Tobias Koenig This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @cond PRIVATE #include "monitor_p.h" #include "collectionfetchjob.h" #include "collectionstatistics.h" #include "itemfetchjob.h" #include "notificationmessage_p.h" #include "session.h" #include using namespace Akonadi; static const int PipelineSize = 5; MonitorPrivate::MonitorPrivate(Monitor * parent) : q_ptr( parent ), nm( 0 ), monitorAll( false ), collectionCache( 3*PipelineSize ), // needs to be at least 3x pipeline size for the collection move case itemCache( PipelineSize ), // needs to be at least 1x pipeline size fetchCollection( false ), fetchCollectionStatistics( false ) { } void MonitorPrivate::init() { QObject::connect( &collectionCache, SIGNAL(dataAvailable()), q_ptr, SLOT(dataAvailable()) ); QObject::connect( &itemCache, SIGNAL(dataAvailable()), q_ptr, SLOT(dataAvailable()) ); } bool MonitorPrivate::connectToNotificationManager() { NotificationMessage::registerDBusTypes(); if ( !nm ) nm = new org::freedesktop::Akonadi::NotificationManager( QLatin1String( "org.freedesktop.Akonadi" ), QLatin1String( "/notifications" ), QDBusConnection::sessionBus(), q_ptr ); else return true; if ( !nm ) { kWarning() << "Unable to connect to notification manager"; } else { QObject::connect( nm, SIGNAL(notify(Akonadi::NotificationMessage::List)), q_ptr, SLOT(slotNotify(Akonadi::NotificationMessage::List)) ); return true; } return false; } int MonitorPrivate::pipelineSize() const { return PipelineSize; } bool MonitorPrivate::acceptNotification(const NotificationMessage & msg) { if ( isSessionIgnored( msg.sessionId() ) ) return false; switch ( msg.type() ) { case NotificationMessage::InvalidType: kWarning() << "Received invalid change notification!"; return false; case NotificationMessage::Item: return isItemMonitored( msg.uid(), msg.parentCollection(), msg.parentDestCollection(), msg.mimeType(), msg.resource() ) || isCollectionMonitored( msg.parentCollection(), msg.resource() ) || isCollectionMonitored( msg.parentDestCollection(), msg.resource() ); case NotificationMessage::Collection: return isCollectionMonitored( msg.uid(), msg.resource() ) || isCollectionMonitored( msg.parentCollection(), msg.resource() ) || isCollectionMonitored( msg.parentDestCollection(), msg.resource() ); } Q_ASSERT( false ); return false; } void MonitorPrivate::dispatchNotifications() { while ( pipeline.size() < pipelineSize() && !pendingNotifications.isEmpty() ) { const NotificationMessage msg = pendingNotifications.dequeue(); if ( ensureDataAvailable( msg ) && pipeline.isEmpty() ) emitNotification( msg ); else pipeline.enqueue( msg ); } } bool MonitorPrivate::ensureDataAvailable( const NotificationMessage &msg ) { bool allCached = true; if ( fetchCollection ) { if ( !collectionCache.ensureCached( msg.parentCollection(), mCollectionFetchScope ) ) allCached = false; if ( msg.operation() == NotificationMessage::Move && !collectionCache.ensureCached( msg.parentDestCollection(), mCollectionFetchScope ) ) allCached = false; } if ( msg.operation() == NotificationMessage::Remove ) return allCached; // the actual object is gone already, nothing to fetch there if ( msg.type() == NotificationMessage::Item && !mItemFetchScope.isEmpty() ) { if ( !itemCache.ensureCached( msg.uid(), mItemFetchScope ) ) allCached = false; } else if ( msg.type() == NotificationMessage::Collection && fetchCollection ) { if ( !collectionCache.ensureCached( msg.uid(), mCollectionFetchScope ) ) allCached = false; } return allCached; } void MonitorPrivate::emitNotification( const NotificationMessage &msg ) { const Collection parent = collectionCache.retrieve( msg.parentCollection() ); Collection destParent; if ( msg.operation() == NotificationMessage::Move ) destParent = collectionCache.retrieve( msg.parentDestCollection() ); if ( msg.type() == NotificationMessage::Collection ) { const Collection col = collectionCache.retrieve( msg.uid() ); emitCollectionNotification( msg, col, parent, destParent ); } else if ( msg.type() == NotificationMessage::Item ) { const Item item = itemCache.retrieve( msg.uid() ); emitItemNotification( msg, item, parent, destParent ); } } void MonitorPrivate::dataAvailable() { while ( !pipeline.isEmpty() ) { const NotificationMessage msg = pipeline.head(); if ( ensureDataAvailable( msg ) ) { emitNotification( msg ); pipeline.dequeue(); } else { break; } } dispatchNotifications(); } void MonitorPrivate::updatePendingStatistics( const NotificationMessage &msg ) { if ( msg.type() == NotificationMessage::Item ) { notifyCollectionStatisticsWatchers( msg.parentCollection(), msg.resource() ); } else if ( msg.type() == NotificationMessage::Collection && msg.operation() == NotificationMessage::Remove ) { // no need for statistics updates anymore recentlyChangedCollections.remove( msg.uid() ); } } void MonitorPrivate::slotSessionDestroyed( QObject * object ) { Session* session = qobject_cast( object ); if ( session ) sessions.removeAll( session->sessionId() ); } void MonitorPrivate::slotStatisticsChangedFinished( KJob* job ) { if ( job->error() ) { kWarning() << "Error on fetching collection statistics: " << job->errorText(); } else { CollectionStatisticsJob *statisticsJob = static_cast( job ); emit q_ptr->collectionStatisticsChanged( statisticsJob->collection().id(), statisticsJob->statistics() ); } } void MonitorPrivate::slotFlushRecentlyChangedCollections() { foreach( Collection::Id collection, recentlyChangedCollections ) { if ( fetchCollectionStatistics ) { fetchStatistics( collection ); } else { static const CollectionStatistics dummyStatistics; emit q_ptr->collectionStatisticsChanged( collection, dummyStatistics ); } } recentlyChangedCollections.clear(); } void MonitorPrivate::slotNotify( const NotificationMessage::List &msgs ) { foreach ( const NotificationMessage &msg, msgs ) { invalidateCaches( msg ); if ( acceptNotification( msg ) ) { updatePendingStatistics( msg ); NotificationMessage::appendAndCompress( pendingNotifications, msg ); } } dispatchNotifications(); } void MonitorPrivate::emitItemNotification( const NotificationMessage &msg, const Item &item, const Collection &collection, const Collection &collectionDest ) { Q_ASSERT( msg.type() == NotificationMessage::Item ); Collection col = collection; Collection colDest = collectionDest; if ( !col.isValid() ) { col = Collection( msg.parentCollection() ); col.setResource( QString::fromUtf8( msg.resource() ) ); } if ( !colDest.isValid() ) { colDest = Collection( msg.parentDestCollection() ); // FIXME setResource here required ? } Item it = item; if ( !it.isValid() || msg.operation() == NotificationMessage::Remove ) { it = Item( msg.uid() ); it.setRemoteId( msg.remoteId() ); it.setMimeType( msg.mimeType() ); } if ( !it.parentCollection().isValid() ) { if ( msg.operation() == NotificationMessage::Move ) it.setParentCollection( colDest ); else it.setParentCollection( col ); } switch ( msg.operation() ) { case NotificationMessage::Add: emit q_ptr->itemAdded( it, col ); break; case NotificationMessage::Modify: emit q_ptr->itemChanged( it, msg.itemParts() ); break; case NotificationMessage::Move: emit q_ptr->itemMoved( it, col, colDest ); break; case NotificationMessage::Remove: emit q_ptr->itemRemoved( it ); emit q_ptr->itemRemoved( it, col ); break; case NotificationMessage::Link: emit q_ptr->itemLinked( it, col ); break; case NotificationMessage::Unlink: emit q_ptr->itemUnlinked( it, col ); break; default: kDebug() << "Unknown operation type" << msg.operation() << "in item change notification"; break; } } void MonitorPrivate::emitCollectionNotification( const NotificationMessage &msg, const Collection &col, const Collection &par, const Collection &dest ) { Q_ASSERT( msg.type() == NotificationMessage::Collection ); Collection collection = col; if ( !collection.isValid() || msg.operation() == NotificationMessage::Remove ) { collection = Collection( msg.uid() ); collection.setResource( QString::fromUtf8( msg.resource() ) ); collection.setRemoteId( msg.remoteId() ); } Collection parent = par; if ( !parent.isValid() ) parent = Collection( msg.parentCollection() ); Collection destination = dest; if ( !destination.isValid() ) destination = Collection( msg.parentDestCollection() ); if ( !collection.parentCollection().isValid() ) { if ( msg.operation() == NotificationMessage::Move ) collection.setParentCollection( destination ); else collection.setParentCollection( parent ); } switch ( msg.operation() ) { case NotificationMessage::Add: emit q_ptr->collectionAdded( collection, parent ); break; case NotificationMessage::Modify: emit q_ptr->collectionChanged( collection ); break; case NotificationMessage::Move: emit q_ptr->collectionMoved( collection, parent, destination ); break; case NotificationMessage::Remove: emit q_ptr->collectionRemoved( collection ); break; default: kDebug() << "Unknown operation type" << msg.operation() << "in collection change notification"; } } void MonitorPrivate::invalidateCaches( const NotificationMessage &msg ) { // remove invalidates if ( msg.operation() == NotificationMessage::Remove ) { if ( msg.type() == NotificationMessage::Collection ) { collectionCache.invalidate( msg.uid() ); } else if ( msg.type() == NotificationMessage::Item ) { itemCache.invalidate( msg.uid() ); } } // modify removes the cache entry, as we need to re-fetch if ( msg.operation() == NotificationMessage::Modify ) { if ( msg.type() == NotificationMessage::Collection ) { collectionCache.update( msg.uid(), mCollectionFetchScope ); } else if ( msg.type() == NotificationMessage::Item ) { itemCache.update( msg.uid(), mItemFetchScope ); } } } +void MonitorPrivate::invalidateCache( const Collection &col ) +{ + collectionCache.update( col.id(), mCollectionFetchScope ); +} + // @endcond diff --git a/akonadi/monitor_p.h b/akonadi/monitor_p.h index 586d688fe..7b9b241eb 100644 --- a/akonadi/monitor_p.h +++ b/akonadi/monitor_p.h @@ -1,164 +1,169 @@ /* Copyright (c) 2007 Tobias Koenig This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_MONITOR_P_H #define AKONADI_MONITOR_P_H #include "monitor.h" #include "collection.h" #include "collectionstatisticsjob.h" #include "collectionfetchscope.h" #include "item.h" #include "itemfetchscope.h" #include "job.h" #include #include "notificationmanagerinterface.h" #include "entitycache_p.h" #include #include #include namespace Akonadi { class Monitor; /** * @internal */ class MonitorPrivate { public: MonitorPrivate( Monitor *parent ); virtual ~MonitorPrivate() {} void init(); Monitor *q_ptr; Q_DECLARE_PUBLIC( Monitor ) org::freedesktop::Akonadi::NotificationManager *nm; Collection::List collections; QSet resources; QSet items; QSet mimetypes; bool monitorAll; QList sessions; ItemFetchScope mItemFetchScope; CollectionFetchScope mCollectionFetchScope; CollectionCache collectionCache; ItemCache itemCache; QQueue pendingNotifications; QQueue pipeline; bool fetchCollection; bool fetchCollectionStatistics; bool isCollectionMonitored( Collection::Id collection, const QByteArray &resource ) const { if ( monitorAll || isCollectionMonitored( collection ) || resources.contains( resource ) ) return true; return false; } bool isItemMonitored( Item::Id item, Collection::Id collection, Collection::Id collectionDest, const QString &mimetype, const QByteArray &resource ) const { if ( monitorAll || isCollectionMonitored( collection ) || isCollectionMonitored( collectionDest ) ||items.contains( item ) || resources.contains( resource ) || isMimeTypeMonitored( mimetype ) ) return true; return false; } bool isSessionIgnored( const QByteArray &sessionId ) const { return sessions.contains( sessionId ); } bool connectToNotificationManager(); bool acceptNotification( const NotificationMessage &msg ); void dispatchNotifications(); bool ensureDataAvailable( const NotificationMessage &msg ); void emitNotification( const NotificationMessage &msg ); void updatePendingStatistics( const NotificationMessage &msg ); void invalidateCaches( const NotificationMessage &msg ); + /** Used by ResourceBase to inform us about collection changes before the notifications are emitted, + needed to avoid the missing RID race on change replay. + */ + void invalidateCache( const Collection &col ); + virtual int pipelineSize() const; // private slots void dataAvailable(); void slotSessionDestroyed( QObject* ); void slotStatisticsChangedFinished( KJob* ); void slotFlushRecentlyChangedCollections(); virtual void slotNotify( const NotificationMessage::List &msgs ); void emitItemNotification( const NotificationMessage &msg, const Item &item = Item(), const Collection &collection = Collection(), const Collection &collectionDest = Collection() ); void emitCollectionNotification( const NotificationMessage &msg, const Collection &col = Collection(), const Collection &par = Collection(), const Collection &dest = Collection() ); private: // collections that need a statistics update QSet recentlyChangedCollections; bool isCollectionMonitored( Collection::Id collection ) const { if ( collections.contains( Collection( collection ) ) ) return true; if ( collections.contains( Collection::root() ) ) return true; return false; } bool isMimeTypeMonitored( const QString& mimetype ) const { if ( mimetypes.contains( mimetype ) ) return true; KMimeType::Ptr mimeType = KMimeType::mimeType( mimetype, KMimeType::ResolveAliases ); if ( mimeType.isNull() ) return false; foreach ( const QString &mt, mimetypes ) { if ( mimeType->is( mt ) ) return true; } return false; } void fetchStatistics( Collection::Id colId ) { CollectionStatisticsJob *job = new CollectionStatisticsJob( Collection( colId ), q_ptr ); QObject::connect( job, SIGNAL(result(KJob*)), q_ptr, SLOT(slotStatisticsChangedFinished(KJob*)) ); } void notifyCollectionStatisticsWatchers( Collection::Id collection, const QByteArray &resource ) { if ( isCollectionMonitored( collection, resource ) ) { if (recentlyChangedCollections.empty() ) QTimer::singleShot( 500, q_ptr, SLOT(slotFlushRecentlyChangedCollections()) ); recentlyChangedCollections.insert( collection ); } } }; } #endif diff --git a/akonadi/resourcebase.cpp b/akonadi/resourcebase.cpp index 60240d29e..5ff6d8c18 100644 --- a/akonadi/resourcebase.cpp +++ b/akonadi/resourcebase.cpp @@ -1,653 +1,662 @@ /* Copyright (c) 2006 Till Adam Copyright (c) 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "resourcebase.h" #include "agentbase_p.h" #include "resourceadaptor.h" #include "collectiondeletejob.h" #include "collectionsync_p.h" #include "itemsync.h" #include "resourcescheduler_p.h" #include "tracerinterface.h" #include "xdgbasedirs_p.h" #include "changerecorder.h" #include "collectionfetchjob.h" #include "collectionfetchscope.h" #include "collectionmodifyjob.h" #include "itemfetchjob.h" #include "itemfetchscope.h" #include "itemmodifyjob.h" #include "itemmodifyjob_p.h" #include "session.h" #include "resourceselectjob_p.h" +#include "monitor_p.h" #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; class Akonadi::ResourceBasePrivate : public AgentBasePrivate { public: ResourceBasePrivate( ResourceBase *parent ) : AgentBasePrivate( parent ), scheduler( 0 ), mItemSyncer( 0 ), mCollectionSyncer( 0 ), mHierarchicalRid( false ) { mStatusMessage = defaultReadyMessage(); } Q_DECLARE_PUBLIC( ResourceBase ) void delayedInit() { if ( !QDBusConnection::sessionBus().registerService( QLatin1String( "org.freedesktop.Akonadi.Resource." ) + mId ) ) kFatal() << "Unable to register service at D-Bus: " << QDBusConnection::sessionBus().lastError().message(); AgentBasePrivate::delayedInit(); } virtual void changeProcessed() { mMonitor->changeProcessed(); if ( !mMonitor->isEmpty() ) scheduler->scheduleChangeReplay(); scheduler->taskDone(); } void slotDeliveryDone( KJob* job ); void slotCollectionSyncDone( KJob *job ); void slotLocalListDone( KJob *job ); void slotSynchronizeCollection( const Collection &col ); void slotCollectionListDone( KJob *job ); void slotItemSyncDone( KJob *job ); void slotPercent( KJob* job, unsigned long percent ); void slotDeleteResourceCollection(); void slotDeleteResourceCollectionDone( KJob *job ); void slotCollectionDeletionDone( KJob *job ); void slotPrepareItemRetrieval( const Akonadi::Item &item ); void slotPrepareItemRetrievalResult( KJob* job ); + void changeCommittedResult( KJob* job ); + // synchronize states Collection currentCollection; ResourceScheduler *scheduler; ItemSync *mItemSyncer; CollectionSync *mCollectionSyncer; bool mHierarchicalRid; }; ResourceBase::ResourceBase( const QString & id ) : AgentBase( new ResourceBasePrivate( this ), id ) { Q_D( ResourceBase ); new ResourceAdaptor( this ); d->scheduler = new ResourceScheduler( this ); d->mMonitor->setChangeRecordingEnabled( true ); connect( d->mMonitor, SIGNAL( changesAdded() ), d->scheduler, SLOT( scheduleChangeReplay() ) ); d->mMonitor->setResourceMonitored( d->mId.toLatin1() ); connect( d->scheduler, SIGNAL( executeFullSync() ), SLOT( retrieveCollections() ) ); connect( d->scheduler, SIGNAL( executeCollectionTreeSync() ), SLOT( retrieveCollections() ) ); connect( d->scheduler, SIGNAL( executeCollectionSync( const Akonadi::Collection& ) ), SLOT( slotSynchronizeCollection( const Akonadi::Collection& ) ) ); connect( d->scheduler, SIGNAL( executeItemFetch( const Akonadi::Item&, const QSet& ) ), SLOT( slotPrepareItemRetrieval(Akonadi::Item)) ); connect( d->scheduler, SIGNAL( executeResourceCollectionDeletion() ), SLOT( slotDeleteResourceCollection() ) ); connect( d->scheduler, SIGNAL( status( int, const QString& ) ), SIGNAL( status( int, const QString& ) ) ); connect( d->scheduler, SIGNAL( executeChangeReplay() ), d->mMonitor, SLOT( replayNext() ) ); connect( d->scheduler, SIGNAL( fullSyncComplete() ), SIGNAL( synchronized() ) ); connect( d->mMonitor, SIGNAL( nothingToReplay() ), d->scheduler, SLOT( taskDone() ) ); connect( this, SIGNAL( synchronized() ), d->scheduler, SLOT( taskDone() ) ); connect( this, SIGNAL( agentNameChanged( const QString& ) ), this, SIGNAL( nameChanged( const QString& ) ) ); d->scheduler->setOnline( d->mOnline ); if ( !d->mMonitor->isEmpty() ) d->scheduler->scheduleChangeReplay(); new ResourceSelectJob( identifier() ); } ResourceBase::~ResourceBase() { } void ResourceBase::synchronize() { d_func()->scheduler->scheduleFullSync(); } void ResourceBase::setName( const QString &name ) { AgentBase::setAgentName( name ); } QString ResourceBase::name() const { return AgentBase::agentName(); } QString ResourceBase::parseArguments( int argc, char **argv ) { QString identifier; if ( argc < 3 ) { kDebug() << "Not enough arguments passed..."; exit( 1 ); } for ( int i = 1; i < argc - 1; ++i ) { if ( QLatin1String( argv[ i ] ) == QLatin1String( "--identifier" ) ) identifier = QLatin1String( argv[ i + 1 ] ); } if ( identifier.isEmpty() ) { kDebug() << "Identifier argument missing"; exit( 1 ); } QByteArray catalog; char *p = strrchr( argv[0], '/' ); if ( p ) catalog = QByteArray( p + 1 ); else catalog = QByteArray( argv[0] ); KCmdLineArgs::init( argc, argv, identifier.toLatin1(), catalog, ki18nc("@title, application name", "Akonadi Resource"), "0.1", ki18nc("@title, application description", "Akonadi Resource") ); KCmdLineOptions options; options.add( "identifier ", ki18nc("@label, commandline option", "Resource identifier") ); KCmdLineArgs::addCmdLineOptions( options ); return identifier; } int ResourceBase::init( ResourceBase *r ) { QApplication::setQuitOnLastWindowClosed( false ); int rv = kapp->exec(); delete r; return rv; } void ResourceBase::itemRetrieved( const Item &item ) { Q_D( ResourceBase ); Q_ASSERT( d->scheduler->currentTask().type == ResourceScheduler::FetchItem ); if ( !item.isValid() ) { QDBusMessage reply( d->scheduler->currentTask().dbusMsg ); reply << false; QDBusConnection::sessionBus().send( reply ); d->scheduler->taskDone(); return; } Item i( item ); QSet requestedParts = d->scheduler->currentTask().itemParts; foreach ( const QByteArray &part, requestedParts ) { if ( !item.loadedPayloadParts().contains( part ) ) { kWarning() << "Item does not provide part" << part; } } ItemModifyJob *job = new ItemModifyJob( i ); // FIXME: remove once the item with which we call retrieveItem() has a revision number job->disableRevisionCheck(); connect( job, SIGNAL( result( KJob* ) ), SLOT( slotDeliveryDone( KJob* ) ) ); } void ResourceBasePrivate::slotDeliveryDone(KJob * job) { Q_Q( ResourceBase ); Q_ASSERT( scheduler->currentTask().type == ResourceScheduler::FetchItem ); QDBusMessage reply( scheduler->currentTask().dbusMsg ); if ( job->error() ) { emit q->error( QLatin1String( "Error while creating item: " ) + job->errorString() ); reply << false; } else { reply << true; } QDBusConnection::sessionBus().send( reply ); scheduler->taskDone(); } void ResourceBasePrivate::slotDeleteResourceCollection() { Q_Q( ResourceBase ); CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::FirstLevel ); job->fetchScope().setResource( q->identifier() ); connect( job, SIGNAL( result( KJob* ) ), q, SLOT( slotDeleteResourceCollectionDone( KJob* ) ) ); } void ResourceBasePrivate::slotDeleteResourceCollectionDone( KJob *job ) { Q_Q( ResourceBase ); if ( job->error() ) { emit q->error( job->errorString() ); scheduler->taskDone(); } else { const CollectionFetchJob *fetchJob = static_cast( job ); if ( !fetchJob->collections().isEmpty() ) { CollectionDeleteJob *job = new CollectionDeleteJob( fetchJob->collections().first() ); connect( job, SIGNAL( result( KJob* ) ), q, SLOT( slotCollectionDeletionDone( KJob* ) ) ); } else { // there is no resource collection, so just ignore the request scheduler->taskDone(); } } } void ResourceBasePrivate::slotCollectionDeletionDone( KJob *job ) { Q_Q( ResourceBase ); if ( job->error() ) { emit q->error( job->errorString() ); } scheduler->taskDone(); } void ResourceBase::changeCommitted( const Item& item ) { Q_D( ResourceBase ); ItemModifyJob *job = new ItemModifyJob( item ); job->d_func()->setClean(); job->disableRevisionCheck(); // TODO: remove, but where/how do we handle the error? job->ignorePayload(); // we only want to reset the dirty flag and update the remote id d->changeProcessed(); } void ResourceBase::changeCommitted( const Collection &collection ) { - Q_D( ResourceBase ); CollectionModifyJob *job = new CollectionModifyJob( collection ); - Q_UNUSED( job ); - //TODO: error checking - d->changeProcessed(); + connect( job, SIGNAL(result(KJob*)), SLOT(changeCommittedResult(KJob*)) ); +} + +void ResourceBasePrivate::changeCommittedResult( KJob *job ) +{ + Q_Q( ResourceBase ); + if ( job->error() ) + emit q->error( i18n( "Updating local collection failed: %1.", job->errorText() ) ); + mMonitor->d_ptr->invalidateCache( static_cast( job )->collection() ); + changeProcessed(); } bool ResourceBase::requestItemDelivery( qint64 uid, const QString & remoteId, const QString &mimeType, const QStringList &_parts ) { Q_D( ResourceBase ); if ( !isOnline() ) { emit error( i18nc( "@info", "Cannot fetch item in offline mode." ) ); return false; } setDelayedReply( true ); // FIXME: we need at least the revision number too Item item( uid ); item.setMimeType( mimeType ); item.setRemoteId( remoteId ); QSet parts; Q_FOREACH( const QString &str, _parts ) parts.insert( str.toLatin1() ); d->scheduler->scheduleItemFetch( item, parts, message().createReply() ); return true; } void ResourceBase::collectionsRetrieved( const Collection::List & collections ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, "ResourceBase::collectionsRetrieved()", "Calling collectionsRetrieved() although no collection retrieval is in progress" ); if ( !d->mCollectionSyncer ) { d->mCollectionSyncer = new CollectionSync( identifier() ); d->mCollectionSyncer->setHierarchicalRemoteIds( d->mHierarchicalRid ); connect( d->mCollectionSyncer, SIGNAL( percent( KJob*, unsigned long ) ), SLOT( slotPercent( KJob*, unsigned long ) ) ); connect( d->mCollectionSyncer, SIGNAL( result( KJob* ) ), SLOT( slotCollectionSyncDone( KJob* ) ) ); } d->mCollectionSyncer->setRemoteCollections( collections ); } void ResourceBase::collectionsRetrievedIncremental( const Collection::List & changedCollections, const Collection::List & removedCollections ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, "ResourceBase::collectionsRetrievedIncremental()", "Calling collectionsRetrievedIncremental() although no collection retrieval is in progress" ); if ( !d->mCollectionSyncer ) { d->mCollectionSyncer = new CollectionSync( identifier() ); d->mCollectionSyncer->setHierarchicalRemoteIds( d->mHierarchicalRid ); connect( d->mCollectionSyncer, SIGNAL( percent( KJob*, unsigned long ) ), SLOT( slotPercent( KJob*, unsigned long ) ) ); connect( d->mCollectionSyncer, SIGNAL( result( KJob* ) ), SLOT( slotCollectionSyncDone( KJob* ) ) ); } d->mCollectionSyncer->setRemoteCollections( changedCollections, removedCollections ); } void ResourceBase::setCollectionStreamingEnabled( bool enable ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, "ResourceBase::setCollectionStreamingEnabled()", "Calling setCollectionStreamingEnabled() although no collection retrieval is in progress" ); if ( !d->mCollectionSyncer ) { d->mCollectionSyncer = new CollectionSync( identifier() ); d->mCollectionSyncer->setHierarchicalRemoteIds( d->mHierarchicalRid ); connect( d->mCollectionSyncer, SIGNAL( percent( KJob*, unsigned long ) ), SLOT( slotPercent( KJob*, unsigned long ) ) ); connect( d->mCollectionSyncer, SIGNAL( result( KJob* ) ), SLOT( slotCollectionSyncDone( KJob* ) ) ); } d->mCollectionSyncer->setStreamingEnabled( enable ); } void ResourceBase::collectionsRetrievalDone() { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, "ResourceBase::collectionsRetrievalDone()", "Calling collectionsRetrievalDone() although no collection retrieval is in progress" ); // streaming enabled, so finalize the sync if ( d->mCollectionSyncer ) { d->mCollectionSyncer->retrievalDone(); } // user did the sync himself, we are done now else { // FIXME: we need the same special case for SyncAll as in slotCollectionSyncDone here! d->scheduler->taskDone(); } } void ResourceBasePrivate::slotCollectionSyncDone( KJob * job ) { Q_Q( ResourceBase ); mCollectionSyncer = 0; if ( job->error() ) { emit q->error( job->errorString() ); } else { if ( scheduler->currentTask().type == ResourceScheduler::SyncAll ) { CollectionFetchJob *list = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive ); list->fetchScope().setResource( mId ); list->fetchScope().setAncestorRetrieval( q->changeRecorder()->collectionFetchScope().ancestorRetrieval() ); q->connect( list, SIGNAL( result( KJob* ) ), q, SLOT( slotLocalListDone( KJob* ) ) ); return; } } scheduler->taskDone(); } void ResourceBasePrivate::slotLocalListDone( KJob * job ) { Q_Q( ResourceBase ); if ( job->error() ) { emit q->error( job->errorString() ); } else { Collection::List cols = static_cast( job )->collections(); foreach ( const Collection &col, cols ) { scheduler->scheduleSync( col ); } scheduler->scheduleFullSyncCompletion(); } scheduler->taskDone(); } void ResourceBasePrivate::slotSynchronizeCollection( const Collection &col ) { Q_Q( ResourceBase ); currentCollection = col; // check if this collection actually can contain anything QStringList contentTypes = currentCollection.contentMimeTypes(); contentTypes.removeAll( Collection::mimeType() ); if ( !contentTypes.isEmpty() ) { emit q->status( AgentBase::Running, i18nc( "@info:status", "Syncing collection '%1'", currentCollection.name() ) ); q->retrieveItems( currentCollection ); return; } scheduler->taskDone(); } void ResourceBasePrivate::slotPrepareItemRetrieval( const Akonadi::Item &item ) { Q_Q( ResourceBase ); ItemFetchJob *fetch = new ItemFetchJob( item, this ); fetch->fetchScope().setAncestorRetrieval( q->changeRecorder()->itemFetchScope().ancestorRetrieval() ); q->connect( fetch, SIGNAL(result(KJob*)), SLOT(slotPrepareItemRetrievalResult(KJob*)) ); } void ResourceBasePrivate::slotPrepareItemRetrievalResult( KJob* job ) { Q_Q( ResourceBase ); Q_ASSERT_X( scheduler->currentTask().type == ResourceScheduler::FetchItem, "ResourceBasePrivate::slotPrepareItemRetrievalResult()", "Preparing item retrieval although no item retrieval is in progress" ); if ( job->error() ) { q->cancelTask( job->errorText() ); return; } ItemFetchJob *fetch = qobject_cast( job ); if ( fetch->items().count() != 1 ) { q->cancelTask( QLatin1String("The requested item does no longer exist") ); return; } const Item item = fetch->items().first(); const QSet parts = scheduler->currentTask().itemParts; if ( !q->retrieveItem( item, parts ) ) q->cancelTask(); } void ResourceBase::itemsRetrievalDone() { Q_D( ResourceBase ); // streaming enabled, so finalize the sync if ( d->mItemSyncer ) { d->mItemSyncer->deliveryDone(); } // user did the sync himself, we are done now else { d->scheduler->taskDone(); } } void ResourceBase::clearCache() { Q_D( ResourceBase ); d->scheduler->scheduleResourceCollectionDeletion(); } Collection ResourceBase::currentCollection() const { Q_D( const ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollection , "ResourceBase::currentCollection()", "Trying to access current collection although no item retrieval is in progress" ); return d->currentCollection; } Item ResourceBase::currentItem() const { Q_D( const ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::FetchItem , "ResourceBase::currentItem()", "Trying to access current item although no item retrieval is in progress" ); return d->scheduler->currentTask().item; } void ResourceBase::synchronizeCollectionTree() { d_func()->scheduler->scheduleCollectionTreeSync(); } void ResourceBase::cancelTask() { Q_D( ResourceBase ); switch ( d->scheduler->currentTask().type ) { case ResourceScheduler::FetchItem: itemRetrieved( Item() ); // sends the error reply and break; case ResourceScheduler::ChangeReplay: d->changeProcessed(); break; default: d->scheduler->taskDone(); } } void ResourceBase::cancelTask( const QString &msg ) { cancelTask(); emit error( msg ); } void ResourceBase::deferTask() { Q_D( ResourceBase ); d->scheduler->deferTask(); } void ResourceBase::doSetOnline( bool state ) { d_func()->scheduler->setOnline( state ); } void ResourceBase::synchronizeCollection( qint64 collectionId ) { CollectionFetchJob* job = new CollectionFetchJob( Collection( collectionId ), CollectionFetchJob::Base ); job->fetchScope().setResource( identifier() ); job->fetchScope().setAncestorRetrieval( changeRecorder()->collectionFetchScope().ancestorRetrieval() ); connect( job, SIGNAL( result( KJob* ) ), SLOT( slotCollectionListDone( KJob* ) ) ); } void ResourceBasePrivate::slotCollectionListDone( KJob *job ) { if ( !job->error() ) { Collection::List list = static_cast( job )->collections(); if ( !list.isEmpty() ) { Collection col = list.first(); scheduler->scheduleSync( col ); } } // TODO: error handling } void ResourceBase::setTotalItems( int amount ) { kDebug() << amount; Q_D( ResourceBase ); setItemStreamingEnabled( true ); d->mItemSyncer->setTotalItems( amount ); } void ResourceBase::setItemStreamingEnabled( bool enable ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollection, "ResourceBase::setItemStreamingEnabled()", "Calling setItemStreamingEnabled() although no item retrieval is in progress" ); if ( !d->mItemSyncer ) { d->mItemSyncer = new ItemSync( currentCollection() ); connect( d->mItemSyncer, SIGNAL( percent( KJob*, unsigned long ) ), SLOT( slotPercent( KJob*, unsigned long ) ) ); connect( d->mItemSyncer, SIGNAL( result( KJob* ) ), SLOT( slotItemSyncDone( KJob* ) ) ); } d->mItemSyncer->setStreamingEnabled( enable ); } void ResourceBase::itemsRetrieved( const Item::List &items ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollection, "ResourceBase::itemsRetrieved()", "Calling itemsRetrieved() although no item retrieval is in progress" ); if ( !d->mItemSyncer ) { d->mItemSyncer = new ItemSync( currentCollection() ); connect( d->mItemSyncer, SIGNAL( percent( KJob*, unsigned long ) ), SLOT( slotPercent( KJob*, unsigned long ) ) ); connect( d->mItemSyncer, SIGNAL( result( KJob* ) ), SLOT( slotItemSyncDone( KJob* ) ) ); } d->mItemSyncer->setFullSyncItems( items ); } void ResourceBase::itemsRetrievedIncremental( const Item::List &changedItems, const Item::List &removedItems ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollection, "ResourceBase::itemsRetrievedIncremental()", "Calling itemsRetrievedIncremental() although no item retrieval is in progress" ); if ( !d->mItemSyncer ) { d->mItemSyncer = new ItemSync( currentCollection() ); connect( d->mItemSyncer, SIGNAL( percent( KJob*, unsigned long ) ), SLOT( slotPercent( KJob*, unsigned long ) ) ); connect( d->mItemSyncer, SIGNAL( result( KJob* ) ), SLOT( slotItemSyncDone( KJob* ) ) ); } d->mItemSyncer->setIncrementalSyncItems( changedItems, removedItems ); } void ResourceBasePrivate::slotItemSyncDone( KJob *job ) { mItemSyncer = 0; Q_Q( ResourceBase ); if ( job->error() ) { emit q->error( job->errorString() ); } scheduler->taskDone(); } void ResourceBasePrivate::slotPercent( KJob *job, unsigned long percent ) { Q_Q( ResourceBase ); Q_UNUSED( job ); emit q->percent( percent ); } void ResourceBase::enableHierarchicalRemoteIdentifiers( bool enable ) { Q_D( ResourceBase ); d->mHierarchicalRid = enable; } #include "resourcebase.moc" diff --git a/akonadi/resourcebase.h b/akonadi/resourcebase.h index 025f4954a..924809d75 100644 --- a/akonadi/resourcebase.h +++ b/akonadi/resourcebase.h @@ -1,475 +1,476 @@ /* This file is part of akonadiresources. Copyright (c) 2006 Till Adam Copyright (c) 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_RESOURCEBASE_H #define AKONADI_RESOURCEBASE_H #include "akonadi_export.h" #include #include #include class KJob; class ResourceAdaptor; namespace Akonadi { class ResourceBasePrivate; /** * @short The base class for all Akonadi resources. * * This class should be used as a base class by all resource agents, * because it encapsulates large parts of the protocol between * resource agent, agent manager and the Akonadi storage. * * It provides many convenience methods to make implementing a * new Akonadi resource agent as simple as possible. * *

How to write a resource

* * The following provides an overview of what you need to do to implement * your own Akonadi resource. In the following, the term 'backend' refers * to the entity the resource connects with Akonadi, be it a single file * or a remote server. * * @todo Complete this (online/offline state management) * *
Basic %Resource Framework
* * The following is needed to create a new resource: * - A new class deriving from Akonadi::ResourceBase, implementing at least all * pure-virtual methods, see below for further details. * - call init() in your main() function. * - a .desktop file similar to the following example * \code * [Desktop Entry] * Encoding=UTF-8 * Name=My Akonadi Resource * Type=AkonadiResource * Exec=akonadi_my_resource * Icon=my-icon * * X-Akonadi-MimeTypes= * X-Akonadi-Capabilities=Resource * X-Akonadi-Identifier=akonadi_my_resource * \endcode * *
Handling PIM Items
* * To follow item changes in the backend, the following steps are necessary: * - Implement retrieveItems() to synchronize all items in the given * collection. If the backend supports incremental retrieval, * implementing support for that is recommended to improve performance. * - Convert the items provided by the backend to Akonadi items. * This typically happens either in retrieveItems() if you retrieved * the collection synchronously (not recommended for network backends) or * in the result slot of the asynchronous retrieval job. * Converting means to create Akonadi::Item objects for every retrieved * item. It's very important that every object has its remote identifier set. * - Call itemsRetrieved() or itemsRetrievedIncremental() respectively * with the item objects created above. The Akonadi storage will then be * updated automatically. Note that it is usually not necessary to manipulate * any item in the Akonadi storage manually. * * To fetch item data on demand, the method retrieveItem() needs to be * reimplemented. Fetch the requested data there and call itemRetrieved() * with the result item. * * To write local changes back to the backend, you need to re-implement * the following three methods: * - itemAdded() * - itemChanged() * - itemRemoved() * Note that these three functions don't get the full payload of the items by default, * you need to change the item fetch scope of the change recorder to fetch the full * payload. This can be expensive with big payloads, though.
* Once you have handled changes in these methods call changeCommitted(). * These methods are called whenever a local item related to this resource is * added, modified or deleted. They are only called if the resource is online, otherwise * all changes are recorded and replayed as soon the resource is online again. * *
Handling Collections
* * To follow collection changes in the backend, the following steps are necessary: * - Implement retrieveCollections() to retrieve collections from the backend. * If the backend supports incremental collections updates, implementing * support for that is recommended to improve performance. * - Convert the collections of the backend to Akonadi collections. * This typically happens either in retrieveCollections() if you retrieved * the collection synchronously (not recommended for network backends) or * in the result slot of the asynchronous retrieval job. * Converting means to create Akonadi::Collection objects for every retrieved * collection. It's very important that every object has its remote identifier * and its parent remote identifier set. * - Call collectionsRetrieved() or collectionsRetrievedIncremental() respectively * with the collection objects created above. The Akonadi storage will then be * updated automatically. Note that it is usually not necessary to manipulate * any collection in the Akonadi storage manually. * * * To write local collection changes back to the backend, you need to re-implement * the following three methods: * - collectionAdded() * - collectionChanged() * - collectionRemoved() * Once you have handled changes in these methods call changeCommitted(). * These methods are called whenever a local collection related to this resource is * added, modified or deleted. They are only called if the resource is online, otherwise * all changes are recorded and replayed as soon the resource is online again. * * @todo Convenience base class for collection-less resources */ // FIXME_API: API dox need to be updated for Observer approach (kevin) class AKONADI_EXPORT ResourceBase : public AgentBase { Q_OBJECT public: /** * Use this method in the main function of your resource * application to initialize your resource subclass. * This method also takes care of creating a KApplication * object and parsing command line arguments. * * @note In case the given class is also derived from AgentBase::Observer * it gets registered as its own observer (see AgentBase::Observer), e.g. * resourceInstance->registerObserver( resourceInstance ); * * @code * * class MyResource : public ResourceBase * { * ... * }; * * int main( int argc, char **argv ) * { * return ResourceBase::init( argc, argv ); * } * * @endcode */ template static int init( int argc, char **argv ) { const QString id = parseArguments( argc, argv ); KApplication app; T* r = new T( id ); // check if T also inherits AgentBase::Observer and // if it does, automatically register it on itself Observer *observer = dynamic_cast( r ); if ( observer != 0 ) r->registerObserver( observer ); return init( r ); } /** * This method is used to set the name of the resource. */ //FIXME_API: make sure location is renamed to this by resourcebase void setName( const QString &name ); /** * Returns the name of the resource. */ QString name() const; Q_SIGNALS: /** * This signal is emitted whenever the name of the resource has changed. * * @param name The new name of the resource. */ void nameChanged( const QString &name ); /** * Emitted when a full synchronization has been completed. */ void synchronized(); protected Q_SLOTS: /** * Retrieve the collection tree from the remote server and supply it via * collectionsRetrieved() or collectionsRetrievedIncremental(). * @see collectionsRetrieved(), collectionsRetrievedIncremental() */ virtual void retrieveCollections() = 0; /** * Retrieve all (new/changed) items in collection @p collection. * It is recommended to use incremental retrieval if the backend supports that * and provide the result by calling itemsRetrievedIncremental(). * If incremental retrieval is not possible, provide the full listing by calling * itemsRetrieved( const Item::List& ). * In any case, ensure that all items have a correctly set remote identifier * to allow synchronizing with items already existing locally. * In case you don't want to use the built-in item syncing code, store the retrieved * items manually and call itemsRetrieved() once you are done. * @param collection The collection whose items to retrieve. * @see itemsRetrieved( const Item::List& ), itemsRetrievedIncremental(), itemsRetrieved(), currentCollection() */ virtual void retrieveItems( const Akonadi::Collection &collection ) = 0; /** * Retrieve a single item from the backend. The item to retrieve is provided as @p item. * Add the requested payload parts and call itemRetrieved() when done. * @param item The empty item whose payload should be retrieved. Use this object when delivering * the result instead of creating a new item to ensure conflict detection will work. * @param parts The item parts that should be retrieved. * @return false if there is an immediate error when retrieving the item. * @see itemRetrieved() */ virtual bool retrieveItem( const Akonadi::Item &item, const QSet &parts ) = 0; protected: /** * Creates a base resource. * * @param id The instance id of the resource. */ ResourceBase( const QString & id ); /** * Destroys the base resource. */ ~ResourceBase(); /** * Call this method from retrieveItem() once the result is available. * * @param item The retrieved item. */ void itemRetrieved( const Item &item ); /** * Resets the dirty flag of the given item and updates the remote id. * * Call whenever you have successfully written changes back to the server. * This implicitly calls changeProcessed(). * @param item The changed item. */ void changeCommitted( const Item &item ); /** * Call whenever you have successfully handled or ignored a collection * change notification. * * This will update the remote identifier of @p collection if necessary, * as well as any other collection attributes. * This implicitly calls changeProcessed(). * @param collection The collection which changes have been handled. */ void changeCommitted( const Collection &collection ); /** * Call this to supply the full folder tree retrieved from the remote server. * * @param collections A list of collections. * @see collectionsRetrievedIncremental() */ void collectionsRetrieved( const Collection::List &collections ); /** * Call this to supply incrementally retrieved collections from the remote server. * * @param changedCollections Collections that have been added or changed. * @param removedCollections Collections that have been deleted. * @see collectionsRetrieved() */ void collectionsRetrievedIncremental( const Collection::List &changedCollections, const Collection::List &removedCollections ); /** * Enable collection streaming, that is collections don't have to be delivered at once * as result of a retrieveCollections() call but can be delivered by multiple calls * to collectionsRetrieved() or collectionsRetrievedIncremental(). When all collections * have been retrieved, call collectionsRetrievalDone(). * @param enable @c true if collection streaming should be enabled, @c false by default */ void setCollectionStreamingEnabled( bool enable ); /** * Call this method to indicate you finished synchronizing the collection tree. * * This is not needed if you use the built in syncing without collection streaming * and call collectionsRetrieved() or collectionRetrievedIncremental() instead. * If collection streaming is enabled, call this method once all collections have been delivered * using collectionsRetrieved() or collectionsRetrievedIncremental(). */ void collectionsRetrievalDone(); /** * Call this method to supply the full collection listing from the remote server. * * If the remote server supports incremental listing, it's strongly * recommended to use itemsRetrievedIncremental() instead. * @param items A list of items. * @see itemsRetrievedIncremental(). */ void itemsRetrieved( const Item::List &items ); /** * Call this method when you want to use the itemsRetrieved() method * in streaming mode and indicate the amount of items that will arrive * that way. * @deprecated Use setItemStreamingEnabled( true ) + itemsRetrieved[Incremental]() * + itemsRetrieved() instead. */ void setTotalItems( int amount ); /** * Enable item streaming. * Item streaming is disabled by default. * @param enable @c true if items are delivered in chunks rather in one big block. */ void setItemStreamingEnabled( bool enable ); /** * Call this method to supply incrementally retrieved items from the remote server. * * @param changedItems Items changed in the backend. * @param removedItems Items removed from the backend. */ void itemsRetrievedIncremental( const Item::List &changedItems, const Item::List &removedItems ); /** * Call this method to indicate you finished synchronizing the current collection. * * This is not needed if you use the built in syncing without item streaming * and call itemsRetrieved() or itemsRetrievedIncremental() instead. * If item streaming is enabled, call this method once all items have been delivered * using itemsRetrieved() or itemsRetrievedIncremental(). * @see retrieveItems() */ void itemsRetrievalDone(); /** * Call this method to remove all items and collections of the resource from the * server cache. * * The method should be used whenever the configuration of the resource has changed * and therefor the cached items might not be valid any longer. * * @since 4.3 */ void clearCache(); /** * Returns the collection that is currently synchronized. */ Collection currentCollection() const; /** * Returns the item that is currently retrieved. */ Item currentItem() const; /** * This method is called whenever the resource should start synchronize all data. */ void synchronize(); /** * This method is called whenever the collection with the given @p id * shall be synchronized. */ void synchronizeCollection( qint64 id ); /** * Refetches the Collections. */ void synchronizeCollectionTree(); /** * Stops the execution of the current task and continues with the next one. */ void cancelTask(); /** * Stops the execution of the current task and continues with the next one. * Additionally an error message is emitted. */ void cancelTask( const QString &error ); /** * Stops the execution of the current task and continues with the next one. * The current task will be tried again later. * * @since 4.3 */ void deferTask(); /** * Inherited from AgentBase. */ void doSetOnline( bool online ); /** * Indicate the use of hierarchical remote identifiers. */ void enableHierarchicalRemoteIdentifiers( bool enable ); private: static QString parseArguments( int, char** ); static int init( ResourceBase *r ); // dbus resource interface friend class ::ResourceAdaptor; bool requestItemDelivery( qint64 uid, const QString &remoteId, const QString &mimeType, const QStringList &parts ); private: Q_DECLARE_PRIVATE( ResourceBase ) Q_PRIVATE_SLOT( d_func(), void slotDeliveryDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotCollectionSyncDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotDeleteResourceCollection() ) Q_PRIVATE_SLOT( d_func(), void slotDeleteResourceCollectionDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotCollectionDeletionDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotLocalListDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotSynchronizeCollection( const Akonadi::Collection& ) ) Q_PRIVATE_SLOT( d_func(), void slotCollectionListDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotItemSyncDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotPercent( KJob*, unsigned long ) ) Q_PRIVATE_SLOT( d_func(), void slotPrepareItemRetrieval( const Akonadi::Item& item ) ) Q_PRIVATE_SLOT( d_func(), void slotPrepareItemRetrievalResult( KJob* ) ) + Q_PRIVATE_SLOT( d_func(), void changeCommittedResult( KJob* ) ) }; } #ifndef AKONADI_RESOURCE_MAIN /** * Convenience Macro for the most common main() function for Akonadi resources. */ #define AKONADI_RESOURCE_MAIN( resourceClass ) \ int main( int argc, char **argv ) \ { \ return Akonadi::ResourceBase::init( argc, argv ); \ } #endif #endif diff --git a/akonadi/tests/entitycachetest.cpp b/akonadi/tests/entitycachetest.cpp index 667f2aa40..e63ecaaf0 100644 --- a/akonadi/tests/entitycachetest.cpp +++ b/akonadi/tests/entitycachetest.cpp @@ -1,136 +1,146 @@ /* Copyright (c) 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "entitycache.cpp" #include #include using namespace Akonadi; class EntityCacheTest : public QObject { Q_OBJECT private: template void testCache() { EntityCache cache( 2 ); QSignalSpy spy( &cache, SIGNAL(dataAvailable()) ); QVERIFY( spy.isValid() ); QVERIFY( !cache.isCached( 1 ) ); QVERIFY( !cache.isRequested( 1 ) ); QVERIFY( !cache.retrieve( 1 ).isValid() ); FetchScope scope; scope.setAncestorRetrieval( FetchScope::All ); cache.request( 1, scope ); QVERIFY( !cache.isCached( 1 ) ); QVERIFY( cache.isRequested( 1 ) ); QVERIFY( !cache.retrieve( 1 ).isValid() ); QTest::qWait( 1000 ); QCOMPARE( spy.count(), 1 ); QVERIFY( cache.isCached( 1 ) ); QVERIFY( cache.isRequested( 1 ) ); const T e1 = cache.retrieve( 1 ); QCOMPARE( e1.id(), 1ll ); QVERIFY( e1.parentCollection().isValid() ); QVERIFY( !e1.parentCollection().remoteId().isEmpty() || e1.parentCollection() == Collection::root() ); spy.clear(); cache.request( 2, FetchScope() ); cache.request( 3, FetchScope() ); QVERIFY( !cache.isCached( 1 ) ); QVERIFY( !cache.isRequested( 1 ) ); QVERIFY( cache.isRequested( 2 ) ); QVERIFY( cache.isRequested( 3 ) ); cache.invalidate( 2 ); QTest::qWait( 1000 ); QCOMPARE( spy.count(), 2 ); QVERIFY( cache.isCached( 2 ) ); QVERIFY( cache.isCached( 3 ) ); const T e2 = cache.retrieve( 2 ); const T e3a = cache.retrieve( 3 ); QCOMPARE( e3a.id(), 3ll ); QVERIFY( !e2.isValid() ); cache.invalidate( 3 ); const T e3b = cache.retrieve( 3 ); QVERIFY( !e3b.isValid() ); spy.clear(); + // updating a cached entry removes it cache.update( 3, FetchScope() ); cache.update( 3, FetchScope() ); QVERIFY( !cache.isCached( 3 ) ); - QVERIFY( cache.isRequested( 3 ) ); + QVERIFY( !cache.isRequested( 3 ) ); QVERIFY( !cache.retrieve( 3 ).isValid() ); + // updating a pending entry re-fetches + cache.request( 3, FetchScope() ); + cache.update( 3, FetchScope() ); + QVERIFY( !cache.isCached( 3 ) ); + QVERIFY( cache.isRequested( 3 ) ); + cache.update( 3, FetchScope() ); + QVERIFY( !cache.isCached( 3 ) ); + QVERIFY( cache.isRequested( 3 ) ); + QTest::qWait( 1000 ); - QCOMPARE( spy.count(), 1 ); + QCOMPARE( spy.count(), 3 ); QVERIFY( cache.isCached( 3 ) ); QVERIFY( cache.retrieve( 3 ).isValid() ); } private slots: void testCacheGeneric_data() { QTest::addColumn( "collection" ); QTest::newRow( "collection" ) << true; QTest::newRow( "item" ) << false; } void testCacheGeneric() { QFETCH( bool, collection ); if ( collection ) testCache(); else testCache(); } void testItemCache() { ItemCache cache( 1 ); QSignalSpy spy( &cache, SIGNAL(dataAvailable()) ); QVERIFY( spy.isValid() ); ItemFetchScope scope; scope.fetchFullPayload( true ); cache.request( 1, scope ); QTest::qWait( 1000 ); QCOMPARE( spy.count(), 1 ); QVERIFY( cache.isCached( 1 ) ); QVERIFY( cache.isRequested( 1 ) ); const Item item = cache.retrieve( 1 ); QCOMPARE( item.id(), 1ll ); QVERIFY( item.hasPayload() ); } }; QTEST_AKONADIMAIN( EntityCacheTest, NoGUI ) #include "entitycachetest.moc" diff --git a/akonadi/tests/fakesession.h b/akonadi/tests/fakesession.h old mode 100755 new mode 100644 diff --git a/kpimidentities/signature.cpp b/kpimidentities/signature.cpp index 5bd23bbe5..eb1e76ca5 100644 --- a/kpimidentities/signature.cpp +++ b/kpimidentities/signature.cpp @@ -1,549 +1,609 @@ /* Copyright (c) 2002-2004 Marc Mutz Copyright (c) 2007 Tom Albers Copyright (c) 2009 Thomas McGuire This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "signature.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KPIMIdentities; class SignaturePrivate { public: struct EmbeddedImage { QImage image; QString name; }; typedef QSharedPointer EmbeddedImagePtr; /// List of images that belong to this signature. Either added by addImage() or /// by readConfig(). QList embeddedImages; /// The directory where the images will be saved to. QString saveLocation; }; QDataStream &operator<< ( QDataStream &stream, const SignaturePrivate::EmbeddedImagePtr &img ) { return stream << img->image << img->name; } QDataStream &operator>> ( QDataStream &stream, SignaturePrivate::EmbeddedImagePtr &img ) { return stream >> img->image >> img->name; } // TODO: KDE5: BIC: Add a real d-pointer. // This QHash is just a workaround around BIC issues, for more info see // http://techbase.kde.org/Policies/Binary_Compatibility_Issues_With_C++ typedef QHash SigPrivateHash; Q_GLOBAL_STATIC(SigPrivateHash, d_func) static SignaturePrivate* d( const Signature *sig ) { SignaturePrivate *ret = d_func()->value( sig, 0 ); if ( !ret ) { ret = new SignaturePrivate; d_func()->insert( sig, ret ); } return ret; } static void delete_d( const Signature* sig ) { SignaturePrivate *ret = d_func()->value( sig, 0 ); delete ret; d_func()->remove( sig ); } Signature::Signature() : mType( Disabled ), mInlinedHtml( false ) {} Signature::Signature( const QString &text ) : mText( text ), mType( Inlined ), mInlinedHtml( false ) {} Signature::Signature( const QString &url, bool isExecutable ) : mUrl( url ), mType( isExecutable ? FromCommand : FromFile ), mInlinedHtml( false ) {} void Signature::assignFrom ( const KPIMIdentities::Signature &that ) { mUrl = that.mUrl; mInlinedHtml = that.mInlinedHtml; mText = that.mText; mType = that.mType; d( this )->saveLocation = d( &that )->saveLocation; d( this )->embeddedImages = d( &that )->embeddedImages; } Signature::Signature ( const Signature &that ) { assignFrom( that ); } Signature& Signature::operator= ( const KPIMIdentities::Signature & that ) { if ( this == &that ) return *this; assignFrom( that ); return *this; } Signature::~Signature() { delete_d( this ); } QString Signature::rawText( bool *ok ) const { switch ( mType ) { case Disabled: if ( ok ) { *ok = true; } return QString(); case Inlined: if ( ok ) { *ok = true; } return mText; case FromFile: return textFromFile( ok ); case FromCommand: return textFromCommand( ok ); }; kFatal(5325) << "Signature::type() returned unknown value!"; return QString(); // make compiler happy } QString Signature::textFromCommand( bool *ok ) const { assert( mType == FromCommand ); // handle pathological cases: if ( mUrl.isEmpty() ) { if ( ok ) { *ok = true; } return QString(); } // create a shell process: KProcess proc; proc.setOutputChannelMode( KProcess::SeparateChannels ); proc.setShellCommand( mUrl ); int rc = proc.execute(); // handle errors, if any: if ( rc != 0 ) { if ( ok ) { *ok = false; } QString wmsg = i18n( "Failed to execute signature script

%1:

" "

%2

", mUrl, QString( proc.readAllStandardError() ) ); KMessageBox::error( 0, wmsg ); return QString(); } // no errors: if ( ok ) { *ok = true; } // get output: QByteArray output = proc.readAllStandardOutput(); // TODO: hmm, should we allow other encodings, too? return QString::fromLocal8Bit( output.data(), output.size() ); } QString Signature::textFromFile( bool *ok ) const { assert( mType == FromFile ); // TODO: Use KIO::NetAccess to download non-local files! if ( !KUrl( mUrl ).isLocalFile() && !( QFileInfo( mUrl ).isRelative() && QFileInfo( mUrl ).exists() ) ) { kDebug(5325) << "Signature::textFromFile:" << "non-local URLs are unsupported"; if ( ok ) { *ok = false; } return QString(); } if ( ok ) { *ok = true; } // TODO: hmm, should we allow other encodings, too? const QByteArray ba = KPIMUtils::kFileToByteArray( mUrl, false ); return QString::fromLocal8Bit( ba.data(), ba.size() ); } QString Signature::withSeparator( bool *ok ) const { QString signature = rawText( ok ); if ( ok && (*ok) == false ) return QString(); if ( signature.isEmpty() ) { return signature; // don't add a separator in this case } - QString newline = ( isInlinedHtml() && mType == Inlined ) ? "
" : "\n"; + const bool htmlSig = ( isInlinedHtml() && mType == Inlined ); + QString newline = htmlSig ? "
" : "\n"; + if ( htmlSig && signature.startsWith( "name; + } + return ret; +} + +void Signature::cleanupImages() const +{ + // Remove any images from the internal structure that are no longer there + if ( isInlinedHtml() ) { + foreach( const SignaturePrivate::EmbeddedImagePtr imageInList, d( this )->embeddedImages ) { + bool found = false; + foreach( const QString &imageInHtml, findImageNames( mText ) ) { + if ( imageInHtml == imageInList->name ) { + found = true; + break; + } + } + if ( !found ) + d( this )->embeddedImages.removeAll( imageInList ); + } + } + + // Delete all the old image files + if ( !d( this )->saveLocation.isEmpty() ) { + QDir dir( d( this )->saveLocation ); + foreach( const QString &fileName, dir.entryList( QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks ) ) { + if ( fileName.toLower().endsWith( ".png" ) ) { + kDebug() << "Deleting old image" << dir.path() + fileName; + dir.remove( fileName ); + } + } + } +} + +void Signature::saveImages() const +{ + if ( isInlinedHtml() && !d( this )->saveLocation.isEmpty() ) { + foreach( const SignaturePrivate::EmbeddedImagePtr &image, d( this )->embeddedImages ) { + QString location = d( this )->saveLocation + '/' + image->name; + if ( !image->image.save( location, "PNG" ) ) { + kWarning() << "Failed to save image" << location; + } + } + } +} + void Signature::readConfig( const KConfigGroup &config ) { QString sigType = config.readEntry( sigTypeKey ); if ( sigType == sigTypeInlineValue ) { mType = Inlined; mInlinedHtml = config.readEntry( sigTypeInlinedHtmlKey, false ); } else if ( sigType == sigTypeFileValue ) { mType = FromFile; mUrl = config.readPathEntry( sigFileKey, QString() ); } else if ( sigType == sigTypeCommandValue ) { mType = FromCommand; mUrl = config.readPathEntry( sigCommandKey, QString() ); } else { mType = Disabled; } mText = config.readEntry( sigTextKey ); d( this )->saveLocation = config.readEntry( sigImageLocation ); if ( isInlinedHtml() && !d( this )->saveLocation.isEmpty() ) { QDir dir( d( this )->saveLocation ); foreach( const QString &fileName, dir.entryList( QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks ) ) { if ( fileName.toLower().endsWith( ".png" ) ) { QImage image; if ( image.load( dir.path() + '/' + fileName ) ) { addImage( image, fileName ); } else { kWarning() << "Unable to load image" << dir.path() + '/' + fileName; } } } } } void Signature::writeConfig( KConfigGroup &config ) const { switch ( mType ) { case Inlined: config.writeEntry( sigTypeKey, sigTypeInlineValue ); config.writeEntry( sigTypeInlinedHtmlKey, mInlinedHtml ); break; case FromFile: config.writeEntry( sigTypeKey, sigTypeFileValue ); config.writePathEntry( sigFileKey, mUrl ); break; case FromCommand: config.writeEntry( sigTypeKey, sigTypeCommandValue ); config.writePathEntry( sigCommandKey, mUrl ); break; case Disabled: config.writeEntry( sigTypeKey, sigTypeDisabledValue ); default: break; } config.writeEntry( sigTextKey, mText ); config.writeEntry( sigImageLocation, d( this )->saveLocation ); - // First delete the old image files - if ( !d( this )->saveLocation.isEmpty() ) { - QDir dir( d( this )->saveLocation ); - foreach( const QString &fileName, dir.entryList( QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks ) ) { - if ( fileName.toLower().endsWith( ".png" ) ) { - kDebug() << "Deleting old image" << dir.path() + fileName; - dir.remove( fileName ); - } - } - } - - // Then, save the new images - if ( isInlinedHtml() && !d( this )->saveLocation.isEmpty() ) { - foreach( const SignaturePrivate::EmbeddedImagePtr &image, d( this )->embeddedImages ) { - QString location = d( this )->saveLocation + '/' + image->name; - if ( !image->image.save( location, "PNG" ) ) { - kWarning() << "Failed to save image" << location; - } - } - } + cleanupImages(); + saveImages(); } void Signature::insertIntoTextEdit( KRichTextEdit *textEdit, Placement placement, bool addSeparator ) { - // Bah. - const_cast( this )->insertIntoTextEdit( textEdit, placement, addSeparator ); + insertIntoTextEdit( textEdit, placement, addSeparator, true ); } void Signature::insertIntoTextEdit( KRichTextEdit *textEdit, - Placement placement, bool addSeparator ) const + Placement placement, bool addSeperator, + bool addNewlines ) const { QString signature; - if ( addSeparator ) + if ( addSeperator ) signature = withSeparator(); else signature = rawText(); insertPlainSignatureIntoTextEdit( signature, textEdit, placement, ( isInlinedHtml() && - type() == KPIMIdentities::Signature::Inlined ) ); + type() == KPIMIdentities::Signature::Inlined ), + addNewlines ); // We added the text of the signature above, now it is time to add the images as well. KPIMTextEdit::TextEdit *pimEdit = dynamic_cast( textEdit ); if ( pimEdit && isInlinedHtml() ) { foreach( const SignaturePrivate::EmbeddedImagePtr &image, d( this )->embeddedImages ) { pimEdit->loadImage( image->image, image->name, image->name ); } } } void Signature::insertPlainSignatureIntoTextEdit( const QString &signature, KRichTextEdit *textEdit, Signature::Placement placement, bool isHtml ) +{ + insertPlainSignatureIntoTextEdit( signature, textEdit, placement, isHtml, true ); +} + +void Signature::insertPlainSignatureIntoTextEdit( const QString &signature, + KRichTextEdit *textEdit, + Placement placement, + bool isHtml, + bool addNewlines ) { if ( !signature.isEmpty() ) { // Save the modified state of the document, as inserting a signature // shouldn't change this. Restore it at the end of this function. bool isModified = textEdit->document()->isModified(); // Move to the desired position, where the signature should be inserted QTextCursor cursor = textEdit->textCursor(); QTextCursor oldCursor = cursor; cursor.beginEditBlock(); if ( placement == End ) cursor.movePosition( QTextCursor::End ); else if ( placement == Start ) cursor.movePosition( QTextCursor::Start ); textEdit->setTextCursor( cursor ); + + QString lineSep; + if ( addNewlines ) { + if ( isHtml ) + lineSep = QLatin1String( "
" ); + else + lineSep = QLatin1Char( '\n' ); + } + // Insert the signature and newlines depending on where it was inserted. bool hackForCursorsAtEnd = false; int oldCursorPos = -1; if ( placement == End ) { if ( oldCursor.position() == textEdit->toPlainText().length() ) { hackForCursorsAtEnd = true; oldCursorPos = oldCursor.position(); } if ( isHtml ) { - textEdit->insertHtml( QLatin1String( "
" ) + signature ); + textEdit->insertHtml( lineSep + signature ); } else { - textEdit->insertPlainText( QLatin1Char( '\n' ) + signature ); + textEdit->insertPlainText( lineSep + signature ); } } else if ( placement == Start || placement == AtCursor ) { if ( isHtml ) { - textEdit->insertHtml( QLatin1String( "
" ) + signature + QLatin1String( "
" ) ); + textEdit->insertHtml( lineSep + signature + lineSep ); } else { - textEdit->insertPlainText( QLatin1Char( '\n' ) + signature + QLatin1Char( '\n' ) ); + textEdit->insertPlainText( lineSep + signature + lineSep ); } } cursor.endEditBlock(); // There is one special case when re-setting the old cursor: The cursor // was at the end. In this case, QTextEdit has no way to know // if the signature was added before or after the cursor, and just decides // that it was added before (and the cursor moves to the end, but it should // not when appending a signature). See bug 167961 if ( hackForCursorsAtEnd ) oldCursor.setPosition( oldCursorPos ); textEdit->setTextCursor( oldCursor ); textEdit->ensureCursorVisible(); textEdit->document()->setModified( isModified ); if ( isHtml ) { textEdit->enableRichTextMode(); } } } // --------------------- Operators -------------------// QDataStream &KPIMIdentities::operator<< ( QDataStream &stream, const KPIMIdentities::Signature &sig ) { return stream << static_cast( sig.mType ) << sig.mUrl << sig.mText << d( &sig )->saveLocation << d( &sig )->embeddedImages; } QDataStream &KPIMIdentities::operator>> ( QDataStream &stream, KPIMIdentities::Signature &sig ) { quint8 s; stream >> s >> sig.mUrl >> sig.mText >> d( &sig )->saveLocation >> d( &sig )->embeddedImages; sig.mType = static_cast( s ); return stream; } bool Signature::operator== ( const Signature &other ) const { if ( mType != other.mType ) { return false; } if ( mType == Inlined && mInlinedHtml ) { if ( d( this )->saveLocation != d( &other )->saveLocation ) return false; if ( d( this )->embeddedImages != d( &other )->embeddedImages ) return false; } switch ( mType ) { case Inlined: return mText == other.mText; case FromFile: case FromCommand: return mUrl == other.mUrl; default: case Disabled: return true; } } QString Signature::plainText() const { QString sigText = rawText(); if ( isInlinedHtml() && type() == Inlined ) { // Use a QTextDocument as a helper, it does all the work for us and // strips all HTML tags. QTextDocument helper; QTextCursor helperCursor( &helper ); helperCursor.insertHtml( sigText ); sigText = helper.toPlainText(); } return sigText; } void Signature::addImage ( const QImage& imageData, const QString& imageName ) { Q_ASSERT( !( d( this )->saveLocation.isEmpty() ) ); SignaturePrivate::EmbeddedImagePtr image( new SignaturePrivate::EmbeddedImage() ); image->image = imageData; image->name = imageName; d( this )->embeddedImages.append( image ); } void Signature::setImageLocation ( const QString& path ) { d( this )->saveLocation = path; } // --------------- Getters -----------------------// QString Signature::text() const { return mText; } QString Signature::url() const { return mUrl; } Signature::Type Signature::type() const { return mType; } // --------------- Setters -----------------------// void Signature::setText( const QString &text ) { mText = text; mType = Inlined; } void Signature::setType( Type type ) { mType = type; } diff --git a/kpimidentities/signature.h b/kpimidentities/signature.h index 0de5aa3dd..700be095c 100644 --- a/kpimidentities/signature.h +++ b/kpimidentities/signature.h @@ -1,265 +1,292 @@ /* Copyright (c) 2002-2004 Marc Mutz Copyright (c) 2007 Tom Albers Copyright (c) 2009 Thomas McGuire Author: Stefan Taferner This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KPIMIDENTITIES_SIGNATURE_H #define KPIMIDENTITIES_SIGNATURE_H #include "kpimidentities_export.h" #include #include #include #include #include #include namespace KPIMIdentities { class Signature; class Identity; } class KConfigGroup; class KRichTextEdit; namespace KPIMIdentities { KPIMIDENTITIES_EXPORT QDataStream &operator<< ( QDataStream &stream, const KPIMIdentities::Signature &sig ); KPIMIDENTITIES_EXPORT QDataStream &operator>> ( QDataStream &stream, KPIMIdentities::Signature &sig ); /** * @short Abstraction of a signature (aka "footer"). * * The signature can either be plain text, HTML text, text returned from a command or text stored * in a file. * * In case of HTML text, the signature can contain images. * Since you set the HTML source with setText(), there also needs to be a way to add the images * to the signature, as the HTML source contains only the img tags that reference those images. * To add the image to the signature, call addImage(). The name given there must match the name * of the img tag in the HTML source. * * The images need to be stored somewhere. The Signature class handles that by storing all images * in a directory. You must set that directory with setImageLocation(), before calling addImage(). * The images added with addImage() are then saved to that directory when calling writeConfig(). * When loading a signature, readConfig() automatically loads the images as well. * To actually add the images to a text edit, call insertIntoTextEdit(). */ class KPIMIDENTITIES_EXPORT Signature { friend class Identity; friend KPIMIDENTITIES_EXPORT QDataStream &operator<< ( QDataStream &stream, const Signature &sig ); friend KPIMIDENTITIES_EXPORT QDataStream &operator>> ( QDataStream &stream, Signature &sig ); public: /** Type of signature (ie. way to obtain the signature text) */ enum Type { Disabled = 0, Inlined = 1, FromFile = 2, FromCommand = 3 }; /** * Describes the placement of the signature text when it is to be inserted into a * text edit */ enum Placement { Start, ///< The signature is placed at the start of the textedit End, ///< The signature is placed at the end of the textedit AtCursor ///< The signature is placed at the current cursor position }; /** Used for comparison */ bool operator== ( const Signature &other ) const; /** Constructor for disabled signature */ Signature(); /** Constructor for inline text */ Signature( const QString &text ); /** Constructor for text from a file or from output of a command */ Signature( const QString &url, bool isExecutable ); /** Copy constructor */ Signature( const Signature &that ); /** Assignment operator */ Signature& operator= ( const Signature &that ); /** Destructor */ ~Signature(); /** @return the raw signature text as entered resp. read from file. */ QString rawText( bool *ok=0 ) const; /** @return the signature text with a "-- \n" separator added, if necessary. A newline will not be appended or prepended. */ QString withSeparator( bool *ok=0 ) const; /** Set the signature text and mark this signature as being of "inline text" type. */ void setText( const QString &text ); QString text() const; /** * Returns the text of the signature. If the signature is HTML, the HTML * tags will be stripped. * @since 4.4 */ QString plainText() const; /** Set the signature URL and mark this signature as being of "from file" resp. "from output of command" type. */ void setUrl( const QString &url, bool isExecutable=false ); QString url() const; /// @return the type of signature (ie. way to obtain the signature text) Type type() const; void setType( Type type ); /** * Sets the inlined signature to text or html * @param isHtml sets the inlined signature to html * @since 4.1 */ void setInlinedHtml( bool isHtml ); /** * @return boolean whether the inlined signature is html * @since 4.1 */ bool isInlinedHtml() const; /** * Sets the location where the copies of the signature images will be stored. * The images will be stored there when calling writeConfig(). The image location * is stored in the config, so the next readConfig() call knows where to look for * images. * It is recommended to use KStandardDirs::locateLocal( "data", "emailidentities/%1" ) * for the location, where %1 is the unique identifier of the identity. * * @warning readConfig will delete all other PNG files in this directory, as they could * be stale inline image files * * Like with addImage(), the SignatureConfigurator will handle this for you. * * @since 4.4 */ void setImageLocation( const QString &path ); /** * Adds the given image to the signature. * This is needed if you use setText() to set some HTML source that references images. Those * referenced images needed to be added by calling this function. The @imageName has to match * the src attribute of the img tag. * * If you use SignatureConfigurator, you don't need to call this function, as the configurator * will handle this for you. * setImageLocation() needs to be called once before. * @since 4.4 */ void addImage( const QImage &image, const QString &imageName ); /** * Inserts this signature into the given text edit. * The cursor position is preserved. * A leading or trailing newline is also added automatically, depending on * the placement. * For undo/redo, this is treated as one operation. * * Rich text mode of the text edit will be enabled if the signature is in * inlined HTML format. * * If this signature uses images, they will be added automatically. * * @param textEdit the signature will be inserted into this text edit. * @param placement defines where in the text edit the signature should be * inserted. * @param addSeparator if true, the separator '-- \n' will be added in front * of the signature * * @since 4.3 * TODO: KDE5: BIC: remove, as we have a const version below * TODO: KDE5: BIC: Change from KRichTextEdit to KPIMTextEdit::TextEdit, to avoid * the dynamic_cast used here */ - void insertIntoTextEdit( KRichTextEdit *textEdit, - Placement placement = End, bool addSeparator = true ); + void KDE_DEPRECATED insertIntoTextEdit( KRichTextEdit *textEdit, + Placement placement = End, bool addSeparator = true ); /** - * Same as the other insertIntoTextEdit(), only that this is the const version + * Same as the other insertIntoTextEdit(), only that this is const and has + * an additional parameter. * @since 4.4 */ void insertIntoTextEdit( KRichTextEdit *textEdit, - Placement placement = End, bool addSeparator = true ) const; + Placement placement = End, bool addSeparator = true, + bool addNewlines = true ) const; /** * Inserts this given signature into the given text edit. * The cursor position is preserved. * A leading or trailing newline is also added automatically, depending on * the placement. * For undo/redo, this is treated as one operation. * A separator is not added. * * Use the insertIntoTextEdit() function if possible, as it has support * for separators and does HTML detection automatically. * * Rich text mode of the text edit will be enabled if @p isHtml is true. * * @param signature the signature, either as plain text or as HTML * @param textEdit the text edit to insert the signature into * @param placement defines where in the textedit the signature should be * inserted. * @param isHtml defines whether the signature should be inserted as text or html * * @since 4.3 + * TODO: KDE5: BIC: remove this method in favor of the overloaded one + */ + static void KDE_DEPRECATED insertPlainSignatureIntoTextEdit( const QString &signature, + KRichTextEdit *textEdit, + Placement placement = End, + bool isHtml = false ); + + /** + * Same as the above, with the possibility to omit linebreaks altogether + * @param addNewlines: If true, will add some newlines before or after the body, depending + * on the placement. Those newlines are useful when this function is + * triggered when the cursor is in the middle of some text. + * @since 4.4 */ static void insertPlainSignatureIntoTextEdit( const QString &signature, KRichTextEdit *textEdit, Placement placement = End, - bool isHtml = false ); + bool isHtml = false, + bool addNewlines = true ); protected: void writeConfig( KConfigGroup &config ) const; void readConfig( const KConfigGroup &config ); /** * Helper used for the copy constructor and the assignment operator */ void assignFrom( const Signature &that ); + /** + * Clean up unused images from our internal list and delete all images + * from the file system + */ + void cleanupImages() const; + + /** + * Saves all images from our internal list to the file system. + */ + void saveImages() const; + private: QString textFromFile( bool *ok ) const; QString textFromCommand( bool *ok ) const; // TODO: KDE5: BIC: Add a d-pointer!!! // There is already a pseude private class in the .cpp, using a hash. QString mUrl; QString mText; Type mType; bool mInlinedHtml; }; } #endif /*kpim_signature_h*/ diff --git a/kpimidentities/signatureconfigurator.cpp b/kpimidentities/signatureconfigurator.cpp index 52fcda16b..4c9c8e8a0 100644 --- a/kpimidentities/signatureconfigurator.cpp +++ b/kpimidentities/signatureconfigurator.cpp @@ -1,465 +1,465 @@ /* -*- c++ -*- Copyright 2008 Thomas McGuire Copyright 2008 Edwin Schepers Copyright 2004 Marc Mutz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "signatureconfigurator.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KPIMIdentities; namespace KPIMIdentities { /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class SignatureConfigurator::Private { public: Private( SignatureConfigurator *parent ); void init(); SignatureConfigurator *q; bool inlinedHtml; HtmlImageMode imageMode; QString imageLocation; }; //@endcond SignatureConfigurator::Private::Private( SignatureConfigurator *parent ) :q( parent ), imageMode( DisableHtmlImages ) { } void SignatureConfigurator::Private::init() { // tmp. vars: QLabel * label; QWidget * page; QHBoxLayout * hlay; QVBoxLayout * vlay; QVBoxLayout * page_vlay; vlay = new QVBoxLayout( q ); vlay->setObjectName( "main layout" ); vlay->setMargin( 0 ); // "enable signatue" checkbox: q->mEnableCheck = new QCheckBox( i18n("&Enable signature"), q ); q->mEnableCheck->setWhatsThis( i18n("Check this box if you want KMail to append a signature to mails " "written with this identity.")); vlay->addWidget( q->mEnableCheck ); // "obtain signature text from" combo and label: hlay = new QHBoxLayout(); // inherits spacing vlay->addLayout( hlay ); q->mSourceCombo = new KComboBox( q ); q->mSourceCombo->setEditable( false ); q->mSourceCombo->setWhatsThis( i18n("Click on the widgets below to obtain help on the input methods.")); q->mSourceCombo->setEnabled( false ); // since !mEnableCheck->isChecked() q->mSourceCombo->addItems( QStringList() << i18nc("continuation of \"obtain signature text from\"", "Input Field Below") << i18nc("continuation of \"obtain signature text from\"", "File") << i18nc("continuation of \"obtain signature text from\"", "Output of Command") ); label = new QLabel( i18n("Obtain signature &text from:"), q ); label->setBuddy( q->mSourceCombo ); label->setEnabled( false ); // since !mEnableCheck->isChecked() hlay->addWidget( label ); hlay->addWidget( q->mSourceCombo, 1 ); // widget stack that is controlled by the source combo: QStackedWidget * widgetStack = new QStackedWidget( q ); widgetStack->setEnabled( false ); // since !mEnableCheck->isChecked() vlay->addWidget( widgetStack, 1 ); q->connect( q->mSourceCombo, SIGNAL(currentIndexChanged(int)), widgetStack, SLOT(setCurrentIndex (int)) ); q->connect( q->mSourceCombo, SIGNAL(highlighted(int)), widgetStack, SLOT(setCurrentIndex (int)) ); // connects for the enabling of the widgets depending on // signatureEnabled: q->connect( q->mEnableCheck, SIGNAL(toggled(bool)), q->mSourceCombo, SLOT(setEnabled(bool)) ); q->connect( q->mEnableCheck, SIGNAL(toggled(bool)), widgetStack, SLOT(setEnabled(bool)) ); q->connect( q->mEnableCheck, SIGNAL(toggled(bool)), label, SLOT(setEnabled(bool)) ); // The focus might be still in the widget that is disabled q->connect( q->mEnableCheck, SIGNAL(clicked()), q->mEnableCheck, SLOT(setFocus()) ); int pageno = 0; // page 0: input field for direct entering: page = new QWidget( widgetStack ); widgetStack->insertWidget( pageno, page ); page_vlay = new QVBoxLayout( page ); q->mEditToolBar = new KToolBar( q ); q->mEditToolBar->setToolButtonStyle( Qt::ToolButtonIconOnly ); page_vlay->addWidget( q->mEditToolBar, 0 ); q->mFormatToolBar = new KToolBar( q ); q->mFormatToolBar->setToolButtonStyle( Qt::ToolButtonIconOnly ); page_vlay->addWidget( q->mFormatToolBar, 1 ); q->mTextEdit = new KPIMTextEdit::TextEdit( q ); static_cast( q->mTextEdit )->enableImageActions(); page_vlay->addWidget( q->mTextEdit, 2 ); q->mTextEdit->setWhatsThis( i18n("Use this field to enter an arbitrary static signature.")); // exclude SupportToPlainText. q->mTextEdit->setRichTextSupport( KRichTextWidget::FullTextFormattingSupport | KRichTextWidget::FullListSupport | KRichTextWidget::SupportAlignment | KRichTextWidget::SupportRuleLine | KRichTextWidget::SupportHyperlinks | KRichTextWidget::SupportFormatPainting ); // Fill the toolbars. KActionCollection *actionCollection = new KActionCollection( q ); q->mTextEdit->createActions( actionCollection ); q->mEditToolBar->addAction( actionCollection->action( "format_text_bold" ) ); q->mEditToolBar->addAction( actionCollection->action( "format_text_italic" ) ); q->mEditToolBar->addAction( actionCollection->action( "format_text_underline" ) ); q->mEditToolBar->addAction( actionCollection->action( "format_text_strikeout" ) ); q->mEditToolBar->addAction( actionCollection->action( "format_text_foreground_color" ) ); q->mEditToolBar->addAction( actionCollection->action( "format_text_background_color" ) ); q->mEditToolBar->addAction( actionCollection->action( "format_font_family" ) ); q->mEditToolBar->addAction( actionCollection->action( "format_font_size" ) ); q->mFormatToolBar->addAction( actionCollection->action( "format_list_style" ) ); q->mFormatToolBar->addAction( actionCollection->action( "format_list_indent_more" ) ); q->mFormatToolBar->addAction( actionCollection->action( "format_list_indent_less" ) ); q->mFormatToolBar->addAction( actionCollection->action( "format_list_indent_less" ) ); q->mFormatToolBar->addSeparator(); q->mFormatToolBar->addAction( actionCollection->action( "format_align_left" ) ); q->mFormatToolBar->addAction( actionCollection->action( "format_align_center" ) ); q->mFormatToolBar->addAction( actionCollection->action( "format_align_right" ) ); q->mFormatToolBar->addAction( actionCollection->action( "format_align_justify" ) ); q->mFormatToolBar->addSeparator(); q->mFormatToolBar->addAction( actionCollection->action( "insert_horizontal_rule" ) ); q->mFormatToolBar->addAction( actionCollection->action( "manage_link" ) ); q->mFormatToolBar->addAction( actionCollection->action( "format_painter" ) ); if ( imageMode == EnableHtmlImages ) { q->mFormatToolBar->addSeparator(); q->mFormatToolBar->addAction( actionCollection->action( "add_image" ) ); } hlay = new QHBoxLayout(); // inherits spacing page_vlay->addLayout( hlay ); q->mHtmlCheck = new QCheckBox( i18n("&Use HTML"), page ); q->connect( q->mHtmlCheck, SIGNAL(clicked()), q, SLOT(slotSetHtml()) ); hlay->addWidget( q->mHtmlCheck ); inlinedHtml = true; widgetStack->setCurrentIndex( 0 ); // since mSourceCombo->currentItem() == 0 // page 1: "signature file" requester, label, "edit file" button: ++pageno; page = new QWidget( widgetStack ); widgetStack->insertWidget( pageno, page ); // force sequential numbers (play safe) page_vlay = new QVBoxLayout( page ); page_vlay->setMargin( 0 ); hlay = new QHBoxLayout(); // inherits spacing page_vlay->addLayout( hlay ); q->mFileRequester = new KUrlRequester( page ); q->mFileRequester->setWhatsThis( i18n("Use this requester to specify a text file that contains your " "signature. It will be read every time you create a new mail or " "append a new signature.")); label = new QLabel( i18n("S&pecify file:"), page ); label->setBuddy( q->mFileRequester ); hlay->addWidget( label ); hlay->addWidget( q->mFileRequester, 1 ); q->mFileRequester->button()->setAutoDefault( false ); q->connect( q->mFileRequester, SIGNAL(textChanged(const QString &)), q, SLOT(slotEnableEditButton(const QString &)) ); q->mEditButton = new QPushButton( i18n("Edit &File"), page ); q->mEditButton->setWhatsThis( i18n("Opens the specified file in a text editor.")); q->connect( q->mEditButton, SIGNAL(clicked()), q, SLOT(slotEdit()) ); q->mEditButton->setAutoDefault( false ); q->mEditButton->setEnabled( false ); // initially nothing to edit hlay->addWidget( q->mEditButton ); page_vlay->addStretch( 1 ); // spacer // page 2: "signature command" requester and label: ++pageno; page = new QWidget( widgetStack ); widgetStack->insertWidget( pageno,page ); page_vlay = new QVBoxLayout( page ); page_vlay->setMargin( 0 ); hlay = new QHBoxLayout(); // inherits spacing page_vlay->addLayout( hlay ); q->mCommandEdit = new KLineEdit( page ); q->mCommandEdit->setCompletionObject( new KShellCompletion() ); q->mCommandEdit->setAutoDeleteCompletionObject( true ); q->mCommandEdit->setWhatsThis( i18n("You can add an arbitrary command here, either with or without path " "depending on whether or not the command is in your Path. For every " "new mail, KMail will execute the command and use what it outputs (to " "standard output) as a signature. Usual commands for use with this " "mechanism are \"fortune\" or \"ksig -random\".")); label = new QLabel( i18n("S&pecify command:"), page ); label->setBuddy( q->mCommandEdit ); hlay->addWidget( label ); hlay->addWidget( q->mCommandEdit, 1 ); page_vlay->addStretch( 1 ); // spacer } SignatureConfigurator::SignatureConfigurator( QWidget * parent ) : QWidget( parent ), d( new Private( this ) ) { d->init(); } SignatureConfigurator::SignatureConfigurator( HtmlImageMode imageMode, QWidget * parent ) : QWidget( parent ), d( new Private( this ) ) { d->imageMode = imageMode; d->init(); } SignatureConfigurator::~SignatureConfigurator() { delete d; } bool SignatureConfigurator::isSignatureEnabled() const { return mEnableCheck->isChecked(); } void SignatureConfigurator::setSignatureEnabled( bool enable ) { mEnableCheck->setChecked( enable ); } Signature::Type SignatureConfigurator::signatureType() const { if ( !isSignatureEnabled() ) return Signature::Disabled; switch ( mSourceCombo->currentIndex() ) { case 0: return Signature::Inlined; case 1: return Signature::FromFile; case 2: return Signature::FromCommand; default: return Signature::Disabled; } } void SignatureConfigurator::setSignatureType( Signature::Type type ) { setSignatureEnabled( type != Signature::Disabled ); int idx = 0; switch( type ) { case Signature::Inlined: idx = 0; break; case Signature::FromFile: idx = 1; break; case Signature::FromCommand: idx = 2; break; default: idx = 0; break; }; mSourceCombo->setCurrentIndex( idx ); } void SignatureConfigurator::setInlineText( const QString & text ) { mTextEdit->setTextOrHtml( text ); } QString SignatureConfigurator::fileURL() const { QString file = mFileRequester->url().path(); // Force the filename to be relative to ~ instead of $PWD depending // on the rest of the code (KRun::run in Edit and KFileItem on save) if ( !file.isEmpty() && QFileInfo( file ).isRelative() ) file = QDir::home().absolutePath() + QDir::separator() + file; return file; } void SignatureConfigurator::setFileURL( const QString & url ) { mFileRequester->setUrl( url ); } QString SignatureConfigurator::commandURL() const { return mCommandEdit->text(); } void SignatureConfigurator::setCommandURL( const QString & url ) { mCommandEdit->setText( url ); } Signature SignatureConfigurator::signature() const { Signature sig; const Signature::Type sigType = signatureType(); switch ( sigType ) { case Signature::Inlined: sig.setInlinedHtml( d->inlinedHtml ); sig.setText( d->inlinedHtml ? asCleanedHTML() : mTextEdit->textOrHtml() ); if ( d->inlinedHtml ) { if ( !d->imageLocation.isEmpty() ) sig.setImageLocation( d->imageLocation ); KPIMTextEdit::ImageWithNameList images = static_cast< KPIMTextEdit::TextEdit*>( mTextEdit )->imagesWithName(); foreach( const KPIMTextEdit::ImageWithNamePtr &image, images ) { sig.addImage( image->image, image->name ); } } break; case Signature::FromCommand: sig.setUrl( commandURL(), true ); break; case Signature::FromFile: sig.setUrl( fileURL(), false ); break; case Signature::Disabled: /* do nothing */ break; } sig.setType( sigType ); return sig; } void SignatureConfigurator::setSignature( const Signature & sig ) { setSignatureType( sig.type() ); if ( sig.isInlinedHtml() ) mHtmlCheck->setCheckState( Qt::Checked ); else mHtmlCheck->setCheckState( Qt::Unchecked ); slotSetHtml(); // Let insertIntoTextEdit() handle setting the text, as that function also adds the images. mTextEdit->clear(); - sig.insertIntoTextEdit( mTextEdit, KPIMIdentities::Signature::Start, false /* no seperator*/ ); + sig.insertIntoTextEdit( mTextEdit, KPIMIdentities::Signature::Start, false, false ); if ( sig.type() == Signature::FromFile ) setFileURL( sig.url() ); else setFileURL( QString() ); if ( sig.type() == Signature::FromCommand ) setCommandURL( sig.url() ); else setCommandURL( QString() ); } void SignatureConfigurator::slotEnableEditButton( const QString & url ) { mEditButton->setDisabled( url.trimmed().isEmpty() ); } void SignatureConfigurator::slotEdit() { QString url = fileURL(); // slotEnableEditButton should prevent this assert from being hit: assert( !url.isEmpty() ); (void)KRun::runUrl( KUrl( url ), QString::fromLatin1("text/plain"), this ); } QString SignatureConfigurator::asCleanedHTML() const { QString text = mTextEdit->toHtml(); // Beautiful little hack to find the html headers produced by Qt. QTextDocument textDocument; QString html = textDocument.toHtml(); // Now remove each line from the text, the result is clean html. foreach( const QString& line, html.split( '\n' ) ){ text.remove( line + '\n' ); } return text; } // "use HTML"-checkbox (un)checked void SignatureConfigurator::slotSetHtml() { if ( mHtmlCheck->checkState() == Qt::Unchecked ) { mHtmlCheck->setText( i18n("&Use HTML") ); mEditToolBar->setVisible( false ); mEditToolBar->setEnabled( false ); mFormatToolBar->setVisible( false ); mFormatToolBar->setEnabled( false ); mTextEdit->switchToPlainText(); d->inlinedHtml = false; } else { mHtmlCheck->setText( i18n("&Use HTML (disabling removes formatting)") ); d->inlinedHtml = true; mEditToolBar->setVisible( true ); mEditToolBar->setEnabled( true ); mFormatToolBar->setVisible( true ); mFormatToolBar->setEnabled( true ); mTextEdit->enableRichTextMode(); } } void SignatureConfigurator::setImageLocation ( const QString& path ) { d->imageLocation = path; } } #include "signatureconfigurator.moc" diff --git a/kpimidentities/tests/signaturetest.cpp b/kpimidentities/tests/signaturetest.cpp index 91d30ed9c..f39062334 100644 --- a/kpimidentities/tests/signaturetest.cpp +++ b/kpimidentities/tests/signaturetest.cpp @@ -1,150 +1,245 @@ /* Copyright (c) 20089 Thomas McGuire This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "qtest_kde.h" #include "signaturetest.h" #include "../signature.h" #include "kpimtextedit/textedit.h" +#include +#include +#include + using namespace KPIMIdentities; using namespace KPIMTextEdit; QTEST_KDEMAIN( SignatureTester, GUI ) void SignatureTester::testSignatures() { Signature sig1; sig1.setText( "Hello World" ); QCOMPARE( sig1.text(), QString( "Hello World" ) ); QCOMPARE( sig1.type(), Signature::Inlined ); QCOMPARE( sig1.rawText(), QString( "Hello World" ) ); QVERIFY( !sig1.isInlinedHtml() ); QCOMPARE( sig1.withSeparator(), QString( "-- \nHello World" ) ); Signature sig2; sig2.setText( "Hello World" ); sig2.setInlinedHtml( true ); QVERIFY( sig2.isInlinedHtml() ); QCOMPARE( sig2.type(), Signature::Inlined ); QCOMPARE( sig2.rawText(), QString( "Hello World" ) ); QCOMPARE( sig2.withSeparator(), QString( "--
Hello World" ) ); // Read this very file in, we use it for the tests QFile thisFile( __FILE__ ); thisFile.open( QIODevice::ReadOnly ); QString fileContent = QString::fromUtf8( thisFile.readAll() ); Signature sig3; sig3.setUrl( QString( "cat " ) + QString( __FILE__ ), true ); QCOMPARE( sig3.rawText(), fileContent ); QVERIFY( !sig3.isInlinedHtml() ); QVERIFY( sig3.text().isEmpty() ); QCOMPARE( sig3.type(), Signature::FromCommand ); QCOMPARE( sig3.withSeparator(), QString( "-- \n" ) + fileContent ); Signature sig4; sig4.setUrl( __FILE__, false ); QCOMPARE( sig4.rawText(), fileContent ); QVERIFY( !sig4.isInlinedHtml() ); QVERIFY( sig4.text().isEmpty() ); QCOMPARE( sig4.type(), Signature::FromFile ); QCOMPARE( sig4.withSeparator(), QString( "-- \n" ) + fileContent ); } static void setCursorPos( QTextEdit &edit, int pos ) { QTextCursor cursor( edit.document() ); cursor.setPosition( pos ); edit.setTextCursor( cursor ); } void SignatureTester::testTextEditInsertion() { TextEdit edit; Signature sig; sig.setText( "Hello World" ); // Test inserting signature at start, with seperators edit.setPlainText( "Bla Bla" ); - sig.insertIntoTextEdit( &edit, Signature::Start, true ); + sig.insertIntoTextEdit( &edit, Signature::Start, true, true ); QVERIFY( edit.textMode() == KRichTextEdit::Plain ); QCOMPARE( edit.toPlainText(), QString( "\n-- \nHello World\nBla Bla" ) ); // Test inserting signature at end. make sure cursor position is preserved edit.clear(); edit.setPlainText( "Bla Bla" ); setCursorPos( edit, 4 ); - sig.insertIntoTextEdit( &edit, Signature::End, true ); + sig.insertIntoTextEdit( &edit, Signature::End, true, true ); QCOMPARE( edit.toPlainText(), QString( "Bla Bla\n-- \nHello World" ) ); QCOMPARE( edit.textCursor().position(), 4 ); // test inserting a signature at cursor position. make sure the cursor // moves the position correctly. make sure modified state is preserved edit.clear(); edit.setPlainText( "Bla Bla" ); setCursorPos( edit, 4 ); edit.document()->setModified( false ); - sig.insertIntoTextEdit( &edit, Signature::AtCursor, true ); + sig.insertIntoTextEdit( &edit, Signature::AtCursor, true, true ); QCOMPARE( edit.toPlainText(), QString( "Bla \n-- \nHello World\nBla" ) ); QCOMPARE( edit.textCursor().position(), 21 ); QVERIFY( !edit.document()->isModified() ); // make sure undo undoes everything in one go edit.undo(); QCOMPARE( edit.toPlainText(), QString( "Bla Bla" ) ); // test inserting signature without seperator. // make sure cursor position and modified state is preserved. edit.clear(); edit.setPlainText( "Bla Bla" ); setCursorPos( edit, 4 ); edit.document()->setModified( true ); - sig.insertIntoTextEdit( &edit, Signature::End, false ); + sig.insertIntoTextEdit( &edit, Signature::End, false, true ); QCOMPARE( edit.toPlainText(), QString( "Bla Bla\nHello World" ) ); QCOMPARE( edit.textCursor().position(), 4 ); QVERIFY( edit.document()->isModified() ); sig.setText( "Hello
World" ); sig.setInlinedHtml( true ); // test that html signatures turn html on and have correct line endings (
vs \n) edit.clear(); edit.setPlainText( "Bla Bla" ); - sig.insertIntoTextEdit( &edit, Signature::End, true ); + sig.insertIntoTextEdit( &edit, Signature::End, true, true ); QVERIFY( edit.textMode() == KRichTextEdit::Rich ); QCOMPARE( edit.toPlainText(), QString( "Bla Bla\n-- \nHello\nWorld" ) ); } void SignatureTester::testBug167961() { TextEdit edit; Signature sig; sig.setText( "BLA" ); // Test that the cursor is still at the start when appending a sig into // an empty text edit - sig.insertIntoTextEdit( &edit, Signature::End, true ); + sig.insertIntoTextEdit( &edit, Signature::End, true, true ); QCOMPARE( edit.textCursor().position(), 0 ); // OTOH, when prepending a sig, the cursor should be at the end edit.clear(); - sig.insertIntoTextEdit( &edit, Signature::Start, true ); + sig.insertIntoTextEdit( &edit, Signature::Start, true, true ); QCOMPARE( edit.textCursor().position(), 9 ); // "\n-- \nBLA\n" } + +// Make writeConfig() public, we need it +class MySignature : public Signature +{ + public: + using Signature::writeConfig; + using Signature::readConfig; +}; + +void SignatureTester::testImages() +{ + TextEdit edit; + QString image1Path = KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ); + QString image2Path = KIconLoader::global()->iconPath( QLatin1String( "arrow-up" ), KIconLoader::Small, false ); + QImage image1, image2; + QVERIFY( image1.load( image1Path ) ); + QVERIFY( image2.load( image1Path ) ); + QString path = KStandardDirs::locateLocal( "data", "emailidentities/unittest/" ); + QString configPath = KStandardDirs::locateLocal( "config", "signaturetest" ); + KConfig config( configPath ); + KConfigGroup group1 = config.group( "Signature1" ); + + MySignature sig; + sig.setImageLocation( path ); + sig.setInlinedHtml( true ); + sig.setText( "BlaBla" ); + sig.addImage( image1, "folder-new.png" ); + sig.writeConfig( group1 ); + + // OK, signature saved, the image should be saved as well + QDir dir( path ); + QStringList entryList = dir.entryList( QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks ); + QCOMPARE( entryList.count(), 1 ); + QCOMPARE( entryList.first(), QString( "folder-new.png" ) ); + + // Now, set the text of the signature to something without images, then save it. + // The signature class should have removed the images. + sig.setText( "ascii ribbon campaign - against html mail" ); + sig.writeConfig( group1 ); + entryList = dir.entryList( QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks ); + QCOMPARE( entryList.count(), 0 ); + + // Enable images again, this time with two of the buggers + sig.setText( "BlaBlaBla" ); + sig.addImage( image1, "folder-new.png" ); + sig.addImage( image2, "arrow-up.png" ); + sig.writeConfig( group1 ); + entryList = dir.entryList( QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks ); + QCOMPARE( entryList.count(), 2 ); + QCOMPARE( entryList.first(), QString( "arrow-up.png" ) ); + QCOMPARE( entryList.last(), QString( "folder-new.png" ) ); + + // Now, create a second signature instance from the same config, and make sure it can still + // read the images, and it does not mess up + MySignature sig2; + sig2.readConfig( group1 ); + sig2.insertIntoTextEdit( &edit, KPIMIdentities::Signature::End, true, true ); + QCOMPARE( edit.embeddedImages().count(), 2 ); + QCOMPARE( sig2.text(), QString( "BlaBlaBla") ); + sig2.writeConfig( group1 ); + entryList = dir.entryList( QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks ); + QCOMPARE( entryList.count(), 2 ); + QCOMPARE( entryList.first(), QString( "arrow-up.png" ) ); + QCOMPARE( entryList.last(), QString( "folder-new.png" ) ); + + // Remove one image from the signature, and make sure only 1 file is left one file system. + sig2.setText( "" ); + sig2.writeConfig( group1 ); + edit.clear(); + sig2.insertIntoTextEdit( &edit, Signature::End, true, true ); + QCOMPARE( edit.embeddedImages().size(), 1 ); + entryList = dir.entryList( QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks ); + QCOMPARE( entryList.count(), 1 ); +} + +void SignatureTester::testLinebreaks() +{ + Signature sig; + sig.setType( Signature::Inlined ); + sig.setInlinedHtml( true ); + sig.setText( "Hans Mustermann
Musterstr. 42" ); + + KPIMTextEdit::TextEdit edit; + sig.insertIntoTextEdit( &edit, Signature::Start, false, false ); + QCOMPARE( edit.toPlainText(), QString( "Hans Mustermann\nMusterstr. 42" ) ); + + sig.setText( "

Hans Mustermann


Musterstr. 42" ); + sig.insertIntoTextEdit( &edit, Signature::Start, true, false ); + QCOMPARE( edit.toPlainText(), QString( "-- \nHans Mustermann\nMusterstr. 42" ) ); +} + diff --git a/kpimidentities/tests/signaturetest.h b/kpimidentities/tests/signaturetest.h index 93c3f8e24..8c942e25d 100644 --- a/kpimidentities/tests/signaturetest.h +++ b/kpimidentities/tests/signaturetest.h @@ -1,35 +1,37 @@ /* Copyright (c) 2009 Thomas McGuire This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef SIGNATURETEST_H #define SIGNATURETEST_H #include class SignatureTester : public QObject { Q_OBJECT private slots: void testSignatures(); void testTextEditInsertion(); void testBug167961(); + void testImages(); + void testLinebreaks(); }; #endif diff --git a/kpimtextedit/tests/textedittest.cpp b/kpimtextedit/tests/textedittest.cpp index cdcf228f4..8eaf4c715 100644 --- a/kpimtextedit/tests/textedittest.cpp +++ b/kpimtextedit/tests/textedittest.cpp @@ -1,380 +1,427 @@ /* Copyright (c) 2009 Thomas McGuire This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "qtest_kde.h" #include "textedittest.h" #include "../textedit.h" #include "kmime/kmime_codecs.h" #include #include #include #include using namespace KPIMTextEdit; QTEST_KDEMAIN( TextEditTester, GUI ) void TextEditTester::testFormattingUsed() { // This method tries to test everything that krichtextedit makes available, so // we can sure that in KMail, when the user uses some formatting, the mail is actually // sent as HTML mail TextEdit textEdit; QVERIFY( !textEdit.isFormattingUsed() ); // Insert some text. QTextCursor cursor( textEdit.document() ); cursor.insertText( QLatin1String( "Hello World!!" ) ); QVERIFY( !textEdit.isFormattingUsed() ); cursor.setPosition( 1 ); textEdit.setTextCursor( cursor ); // // Test link // QString someUrl = QLatin1String( "www.test.de" ); QString altText = QLatin1String( "Hello" ); textEdit.updateLink( someUrl, altText ); QVERIFY( textEdit.isFormattingUsed() ); QCOMPARE( textEdit.currentLinkUrl(), someUrl ); QCOMPARE( textEdit.currentLinkText(), altText ); cursor.setPosition( 1 ); textEdit.setTextCursor( cursor ); textEdit.updateLink( QString(), QLatin1String( "Hello" ) ); QVERIFY( textEdit.currentLinkUrl().isEmpty() ); QVERIFY( !textEdit.currentLinkText().isEmpty() ); QVERIFY( !textEdit.isFormattingUsed() ); // // Test alignment // cursor.setPosition( 1 ); textEdit.setTextCursor( cursor ); textEdit.alignRight(); QVERIFY( textEdit.isFormattingUsed() ); QCOMPARE( textEdit.alignment(), Qt::AlignRight ); textEdit.alignLeft(); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.alignCenter(); QCOMPARE( textEdit.alignment(), Qt::AlignHCenter ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.alignJustify(); QCOMPARE( textEdit.alignment(), Qt::AlignJustify ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.alignLeft(); QCOMPARE( textEdit.alignment(), Qt::AlignLeft ); QVERIFY( !textEdit.isFormattingUsed() ); // // Test lists // textEdit.setListStyle( QTextListFormat::ListCircle ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setListStyle( 0 ); QVERIFY( !textEdit.isFormattingUsed() ); // // Test font attributes // textEdit.setFontFamily( QLatin1String( "Times" ) ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setFontFamily( textEdit.document()->defaultFont().family() ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setFontSize( 48 ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setFontSize( textEdit.document()->defaultFont().pointSize() ); QVERIFY( !textEdit.isFormattingUsed() ); QFont myFont = textEdit.document()->defaultFont(); myFont.setStyle( QFont::StyleOblique ); textEdit.setFont( myFont ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setFont( textEdit.document()->defaultFont() ); QVERIFY( !textEdit.isFormattingUsed() ); // // Test bold, italic, underline and strikeout // textEdit.setTextBold( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextBold( false ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setTextUnderline( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextUnderline( false ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setTextItalic( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextItalic( false ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setTextStrikeOut( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextStrikeOut( false ); QVERIFY( !textEdit.isFormattingUsed() ); // // Color // QColor oldForeground = textEdit.document()->firstBlock().charFormat().foreground().color(); textEdit.setTextForegroundColor( Qt::red ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextForegroundColor( oldForeground ); QVERIFY( !textEdit.isFormattingUsed() ); QColor oldBackground = textEdit.document()->firstBlock().charFormat().background().color(); textEdit.setTextBackgroundColor( Qt::red ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextBackgroundColor( oldBackground ); QVERIFY( !textEdit.isFormattingUsed() ); // // Horizontal rule // textEdit.insertHorizontalRule(); QVERIFY( textEdit.isFormattingUsed() ); // No way to easily remove the horizontal line, so clear the text edit and start over textEdit.clear(); cursor.insertText( QLatin1String( "Hello World!!" ) ); QVERIFY( !textEdit.isFormattingUsed() ); cursor.setPosition( 1 ); textEdit.setTextCursor( cursor ); // // Sub and superscript // textEdit.setTextSuperScript( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextSuperScript( false ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setTextSubScript( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextSubScript( false ); QVERIFY( !textEdit.isFormattingUsed() ); // // Image // QString imagePath = KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ); textEdit.addImage( imagePath ); QVERIFY( textEdit.isFormattingUsed() ); cursor = textEdit.textCursor(); cursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, 1 ); cursor.removeSelectedText(); QVERIFY( !textEdit.isFormattingUsed() ); } void TextEditTester::testQuoting() { TextEdit edit; QVERIFY( edit.isLineQuoted( QLatin1String( "> Hello" ) ) ); QVERIFY( edit.isLineQuoted( QLatin1String( ">Hello" ) ) ); QVERIFY( !edit.isLineQuoted( QLatin1String( "Hello" ) ) ); QCOMPARE( edit.quoteLength( QLatin1String( "Hello" ) ), 0 ); QCOMPARE( edit.quoteLength( QLatin1String( ">Hello" ) ), 1 ); QCOMPARE( edit.quoteLength( QLatin1String( "> Hello" ) ), 2 ); QCOMPARE( edit.quoteLength( QLatin1String( ">>>Hello" ) ), 3 ); QCOMPARE( edit.quoteLength( QLatin1String( "> > > Hello" ) ), 6 ); QCOMPARE( edit.quoteLength( QLatin1String( "|Hello" ) ), 1 ); QCOMPARE( edit.quoteLength( QLatin1String( "| |Hello" ) ), 3 ); } void TextEditTester::testCleanText() { TextEdit edit; QLatin1String html( "Heelllo World
Bye!" ); QLatin1String plain( "Heelllo World\nBye!" ); edit.setTextOrHtml( html ); edit.addImage( KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ) ); QVERIFY( edit.textMode() == TextEdit::Rich ); QCOMPARE( edit.toCleanPlainText(), plain ); edit.show(); // < otherwise toWrappedPlainText can't work, it needs a layout QCOMPARE( edit.toWrappedPlainText(), plain ); } void TextEditTester::testEnter_data() { QTest::addColumn("initalText"); QTest::addColumn("expectedText"); QTest::addColumn("cursorPos"); QTest::newRow( "" ) << QString::fromAscii( "> Hello World" ) << QString::fromAscii( "> Hello \n> World" ) << 8; QTest::newRow( "" ) << QString::fromAscii( "Hello World" ) << QString::fromAscii( "Hello \nWorld" ) << 6; QTest::newRow( "" ) << QString::fromAscii( "> Hello World" ) << QString::fromAscii( "> Hello World\n" ) << 13; QTest::newRow( "" ) << QString::fromAscii( ">Hello World" ) << QString::fromAscii( ">Hello \n>World" ) << 7; QTest::newRow( "" ) << QString::fromAscii( "> > Hello World" ) << QString::fromAscii( "> > Hello \n> > World" ) << 10; QTest::newRow( "" ) << QString::fromAscii( "| | Hello World" ) << QString::fromAscii( "| | Hello \n| | World" ) << 10; } void TextEditTester::testEnter() { QFETCH( QString, initalText ); QFETCH( QString, expectedText ); QFETCH( int, cursorPos ); TextEdit edit; edit.setPlainText( initalText ); QTextCursor textCursor( edit.document() ); textCursor.setPosition( cursorPos ); edit.setTextCursor( textCursor ); QTest::keyClick( &edit, Qt::Key_Return ); QCOMPARE( edit.toPlainText(), expectedText ); } void TextEditTester::testImages() { TextEdit edit; QString image1Path = KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ); QString image2Path = KIconLoader::global()->iconPath( QLatin1String( "arrow-up" ), KIconLoader::Small, false ); // Add one image, check that embeddedImages() returns the right stuff edit.addImage( image1Path ); KPIMTextEdit::ImageList images = edit.embeddedImages(); + KPIMTextEdit::ImageWithNameList imagesWithNames = edit.imagesWithName(); QCOMPARE( images.size(), 1 ); + QCOMPARE( imagesWithNames.size(), 1 ); EmbeddedImage *image = images.first().data(); + ImageWithName *imageWithName = imagesWithNames.first().data(); QCOMPARE( image->imageName, QString::fromAscii( "folder-new.png" ) ); + QCOMPARE( imageWithName->name, QString::fromAscii( "folder-new.png" ) ); // Also check that it loads the correct image QImage diskImage( image1Path ); QBuffer buffer; buffer.open( QIODevice::WriteOnly ); diskImage.save( &buffer, "PNG" ); + QBuffer imageWithNameBuffer; + imageWithNameBuffer.open( QIODevice::WriteOnly ); + imageWithName->image.save( &imageWithNameBuffer, "PNG" ); QByteArray encodedImage = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() ); QCOMPARE( image->image, encodedImage ); + QCOMPARE( buffer.buffer(), imageWithNameBuffer.buffer() ); // No image should be there after clearing edit.clear(); QVERIFY( edit.embeddedImages().isEmpty() ); + QVERIFY( edit.imagesWithName().isEmpty() ); // Check that manually removing the image also empties the image list edit.addImage( image1Path ); QCOMPARE( edit.embeddedImages().size(), 1 ); + QCOMPARE( edit.imagesWithName().size(), 1 ); QTextCursor cursor = edit.textCursor(); cursor.setPosition( 0, QTextCursor::MoveAnchor ); cursor.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, 1 ); cursor.removeSelectedText(); QVERIFY( edit.embeddedImages().isEmpty() ); + QVERIFY( edit.imagesWithName().isEmpty() ); // Check that adding the identical image two times only adds the image once edit.addImage( image1Path ); edit.addImage( image1Path ); QCOMPARE( edit.embeddedImages().size(), 1 ); + QCOMPARE( edit.imagesWithName().size(), 1 ); // Another different image added, and we should have two images edit.clear(); edit.addImage( image1Path ); edit.addImage( image2Path ); images = edit.embeddedImages(); + imagesWithNames = edit.imagesWithName(); QCOMPARE( images.size(), 2 ); + QCOMPARE( imagesWithNames.size(), 2 ); KPIMTextEdit::EmbeddedImage *image1 = images.first().data(); KPIMTextEdit::EmbeddedImage *image2 = images.last().data(); + KPIMTextEdit::ImageWithName *imageWithName1 = imagesWithNames.first().data(); + KPIMTextEdit::ImageWithName *imageWithName2 = imagesWithNames.last().data(); QCOMPARE( image1->imageName, QString::fromAscii( "folder-new2.png" ) ); // ### FIXME: should be folder-new.png, but QTextEdit provides no way to remove cached resources! + QCOMPARE( imageWithName1->name, QString::fromAscii( "folder-new2.png" ) ); QCOMPARE( image2->imageName, QString::fromAscii( "arrow-up.png" ) ); + QCOMPARE( imageWithName2->name, QString::fromAscii( "arrow-up.png" ) ); QVERIFY( image1->contentID != image2->contentID ); } void TextEditTester::testImageHtmlCode() { TextEdit edit; QString image1Path = KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ); QString image2Path = KIconLoader::global()->iconPath( QLatin1String( "arrow-up" ), KIconLoader::Small, false ); edit.addImage( image1Path ); edit.addImage( image2Path ); KPIMTextEdit::ImageList images = edit.embeddedImages(); QCOMPARE( images.size(), 2 ); KPIMTextEdit::EmbeddedImage *image1 = images.first().data(); KPIMTextEdit::EmbeddedImage *image2 = images.last().data(); QString startHtml = QLatin1String( "BlaBlub" ); QString endHtml = QString( QLatin1String( "BlaBlub" ) ) .arg( image2->contentID ).arg( image1->contentID ); QCOMPARE( TextEdit::imageNamesToContentIds( startHtml.toAscii(), images ), endHtml.toAscii() ); } void TextEditTester::testDeleteLine_data() { QTest::addColumn("initalText"); QTest::addColumn("expectedText"); QTest::addColumn("cursorPos"); QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) << QString::fromAscii( "line1\nline3" ) << 6; QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) << QString::fromAscii( "line2\nline3" ) << 5; QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) << QString::fromAscii( "line1\nline3" ) << 11; QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) << QString::fromAscii( "line2\nline3" ) << 0; QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) << QString::fromAscii( "line1\nline2" ) << 17; QTest::newRow( "" ) << QString::fromAscii( "line1" ) << QString::fromAscii( "" ) << 0; QTest::newRow( "" ) << QString::fromAscii( "line1" ) << QString::fromAscii( "" ) << 5; // Now, test deletion with word wrapping. The line with the Ms is so long that it will get wrapped QTest::newRow( "" ) << QString::fromAscii( "line1\nMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3" ) << QString::fromAscii( "line1\nMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3" ) << 6; QTest::newRow( "" ) << QString::fromAscii( "line1\nMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3" ) << QString::fromAscii( "line1\nMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3" ) << 13; } void TextEditTester::testDeleteLine() { QFETCH( QString, initalText ); QFETCH( QString, expectedText ); QFETCH( int, cursorPos ); TextEdit edit; edit.setPlainText( initalText ); QTextCursor cursor = edit.textCursor(); cursor.setPosition( cursorPos ); edit.setTextCursor( cursor ); edit.show(); // we need a layout for this to work edit.deleteCurrentLine(); QCOMPARE( edit.toPlainText(), expectedText ); } +void TextEditTester::testLoadImage() +{ + TextEdit edit; + QString image1Path = KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ); + QString image2Path = KIconLoader::global()->iconPath( QLatin1String( "arrow-up" ), KIconLoader::Small, false ); + QImage image1, image2; + QVERIFY( image1.load( image1Path ) ); + QVERIFY( image2.load( image1Path ) ); + + edit.setHtml( QLatin1String( "BlaBla" ) ); + + // First try to load an image with a name that doesn't match, it should fail + edit.loadImage( image1, QString::fromLatin1( "doesntmatch" ), QString::fromLatin1( "folder-new" ) ); + QVERIFY( !edit.document()->resource( QTextDocument::ImageResource, QUrl( QLatin1String( "folder-new" ) ) ).isValid() ); + + // Now, load the image for real + edit.loadImage( image1, QString::fromLatin1( "folder-new.png" ), QString::fromLatin1( "folder-new" ) ); + QVERIFY( edit.document()->resource( QTextDocument::ImageResource, QUrl( QLatin1String( "folder-new" ) ) ).isValid() ); + + // New test with a new textedit (so that we don't use the cached resources + // This example has two images in the same text block, make sure that doesn't crash (bug 204214) + TextEdit edit2; + edit2.setHtml( QLatin1String( "" ) ); + edit2.loadImage( image1, QString::fromLatin1( "folder-new.png" ), QString::fromLatin1( "folder-new" ) ); + QVERIFY( edit.document()->resource( QTextDocument::ImageResource, QUrl( QLatin1String( "folder-new" ) ) ).isValid() ); + QCOMPARE( edit.embeddedImages().size(), 1 ); +} + + diff --git a/kpimtextedit/tests/textedittest.h b/kpimtextedit/tests/textedittest.h index dc7a86b90..c961f9b6a 100644 --- a/kpimtextedit/tests/textedittest.h +++ b/kpimtextedit/tests/textedittest.h @@ -1,41 +1,42 @@ /* Copyright (c) 2009 Thomas McGuire This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef TEXTEDITTEST_H #define TEXTEDITTEST_H #include class TextEditTester : public QObject { Q_OBJECT private slots: void testFormattingUsed(); void testQuoting(); void testCleanText(); void testEnter(); void testEnter_data(); void testImages(); void testImageHtmlCode(); void testDeleteLine(); void testDeleteLine_data(); + void testLoadImage(); }; #endif diff --git a/kpimutils/linklocator.cpp b/kpimutils/linklocator.cpp index 37338b735..a98057597 100644 --- a/kpimutils/linklocator.cpp +++ b/kpimutils/linklocator.cpp @@ -1,424 +1,452 @@ /* Copyright (c) 2002 Dave Corrie This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** @file This file is part of the KDEPIM Utilities library and provides the LinkLocator class. @brief Identifies URLs and email addresses embedded in plaintext. @author Dave Corrie \ */ #include "linklocator.h" #include #include #include #include #include #if KDE_IS_VERSION( 4, 0, 95 ) #include #endif #include #include #include #include #include using namespace KPIMUtils; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class KPIMUtils::LinkLocator::Private { public: int mMaxUrlLen; int mMaxAddressLen; }; //@endcond #if KDE_IS_VERSION( 4, 0, 95 ) // Use a static for this as calls to the KEmoticons constructor are expensive. K_GLOBAL_STATIC( KEmoticons, sEmoticons ) #endif LinkLocator::LinkLocator( const QString &text, int pos ) : mText( text ), mPos( pos ), d( new KPIMUtils::LinkLocator::Private ) { d->mMaxUrlLen = 4096; d->mMaxAddressLen = 255; // If you change either of the above values for maxUrlLen or // maxAddressLen, then please also update the documentation for // setMaxUrlLen()/setMaxAddressLen() in the header file AND the // default values used for the maxUrlLen/maxAddressLen parameters // of convertToHtml(). } LinkLocator::~LinkLocator() { delete d; } void LinkLocator::setMaxUrlLen( int length ) { d->mMaxUrlLen = length; } int LinkLocator::maxUrlLen() const { return d->mMaxUrlLen; } void LinkLocator::setMaxAddressLen( int length ) { d->mMaxAddressLen = length; } int LinkLocator::maxAddressLen() const { return d->mMaxAddressLen; } QString LinkLocator::getUrl() { QString url; if ( atUrl() ) { // for reference: rfc1738: // Thus, only alphanumerics, the special characters "$-_.+!*'(),", and // reserved characters used for their reserved purposes may be used // unencoded within a URL. // NOTE: this implementation is not RFC conforming int start = mPos; while ( mPos < (int)mText.length() && mText[mPos] > ' ' && mText[mPos] != '"' && QString( "<>[]" ).indexOf( mText[mPos] ) == -1 ) { ++mPos; } // some URLs really end with: # / & - _ const QString allowedSpecialChars = QString( "#/&-_" ); while ( mPos > start && mText[mPos-1].isPunct() && allowedSpecialChars.indexOf( mText[mPos-1] ) == -1 ) { --mPos; } url = mText.mid( start, mPos - start ); if ( isEmptyUrl(url) || mPos - start > maxUrlLen() ) { mPos = start; url = ""; } else { --mPos; } } return url; } // keep this in sync with KMMainWin::slotUrlClicked() bool LinkLocator::atUrl() const { // the following characters are allowed in a dot-atom (RFC 2822): // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~ const QString allowedSpecialChars = QString( ".!#$%&'*+-/=?^_`{|}~" ); // the character directly before the URL must not be a letter, a number or // any other character allowed in a dot-atom (RFC 2822). if ( ( mPos > 0 ) && ( mText[mPos-1].isLetterOrNumber() || ( allowedSpecialChars.indexOf( mText[mPos-1] ) != -1 ) ) ) { return false; } QChar ch = mText[mPos]; return ( ch == 'h' && ( mText.mid( mPos, 7 ) == "http://" || mText.mid( mPos, 8 ) == "https://" ) ) || ( ch == 'v' && mText.mid( mPos, 6 ) == "vnc://" ) || ( ch == 'f' && ( mText.mid( mPos, 7 ) == "fish://" || mText.mid( mPos, 6 ) == "ftp://" || mText.mid( mPos, 7 ) == "ftps://" ) ) || ( ch == 's' && ( mText.mid( mPos, 7 ) == "sftp://" || mText.mid( mPos, 6 ) == "smb://" ) ) || ( ch == 'm' && mText.mid( mPos, 7 ) == "mailto:" ) || ( ch == 'w' && mText.mid( mPos, 4 ) == "www." ) || ( ch == 'f' && ( mText.mid( mPos, 4 ) == "ftp." || mText.mid( mPos, 7 ) == "file://" ) ) || ( ch == 'n' && mText.mid( mPos, 5 ) == "news:" ); } bool LinkLocator::isEmptyUrl( const QString &url ) const { return url.isEmpty() || url == "http://" || url == "https://" || url == "fish://" || url == "ftp://" || url == "ftps://" || url == "sftp://" || url == "smb://" || url == "vnc://" || url == "mailto" || url == "www" || url == "ftp" || url == "news" || url == "news://"; } QString LinkLocator::getEmailAddress() { QString address; if ( mText[mPos] == '@' ) { // the following characters are allowed in a dot-atom (RFC 2822): // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~ const QString allowedSpecialChars = QString( ".!#$%&'*+-/=?^_`{|}~" ); // determine the local part of the email address int start = mPos - 1; while ( start >= 0 && mText[start].unicode() < 128 && ( mText[start].isLetterOrNumber() || mText[start] == '@' || // allow @ to find invalid email addresses allowedSpecialChars.indexOf( mText[start] ) != -1 ) ) { if ( mText[start] == '@' ) { return QString(); // local part contains '@' -> no email address } --start; } ++start; // we assume that an email address starts with a letter or a digit while ( ( start < mPos ) && !mText[start].isLetterOrNumber() ) { ++start; } if ( start == mPos ) { return QString(); // local part is empty -> no email address } // determine the domain part of the email address int dotPos = INT_MAX; int end = mPos + 1; while ( end < (int)mText.length() && ( mText[end].isLetterOrNumber() || mText[end] == '@' || // allow @ to find invalid email addresses mText[end] == '.' || mText[end] == '-' ) ) { if ( mText[end] == '@' ) { return QString(); // domain part contains '@' -> no email address } if ( mText[end] == '.' ) { dotPos = qMin( dotPos, end ); // remember index of first dot in domain } ++end; } // we assume that an email address ends with a letter or a digit while ( ( end > mPos ) && !mText[end - 1].isLetterOrNumber() ) { --end; } if ( end == mPos ) { return QString(); // domain part is empty -> no email address } if ( dotPos >= end ) { return QString(); // domain part doesn't contain a dot } if ( end - start > maxAddressLen() ) { return QString(); // too long -> most likely no email address } address = mText.mid( start, end - start ); mPos = end - 1; } return address; } QString LinkLocator::convertToHtml( const QString &plainText, int flags, int maxUrlLen, int maxAddressLen ) { LinkLocator locator( plainText ); locator.setMaxUrlLen( maxUrlLen ); locator.setMaxAddressLen( maxAddressLen ); QString str; QString result( (QChar*)0, (int)locator.mText.length() * 2 ); QChar ch; int x; bool startOfLine = true; QString emoticon; for ( locator.mPos = 0, x = 0; locator.mPos < (int)locator.mText.length(); locator.mPos++, x++ ) { ch = locator.mText[locator.mPos]; if ( flags & PreserveSpaces ) { if ( ch == ' ' ) { + if ( locator.mPos + 1 < locator.mText.length() ) { + if ( locator.mText[locator.mPos + 1] != ' ' ) { + + // A single space, make it breaking if not at the start or end of the line + const bool endOfLine = locator.mText[locator.mPos + 1] == '\n'; + if ( !startOfLine && !endOfLine ) + result += ' '; + else + result += " "; + } + else { + + // Whitespace of more than one space, make it all non-breaking + while( locator.mPos < locator.mText.length() && locator.mText[locator.mPos] == ' ' ) { + result += " "; + locator.mPos++; + x++; + } + + // We incremented once to often, undo that + locator.mPos--; + x--; + } + } + else { + // Last space in the text, it is non-breaking + result += " "; + } + if ( startOfLine ) { startOfLine = false; } - result += " "; continue; } else if ( ch == '\t' ) { do { result += " "; x++; } while ( ( x & 7 ) != 0 ); x--; startOfLine = false; continue; } } if ( ch == '\n' ) { result += "
\n"; // Keep the \n, so apps can figure out the quoting levels correctly. startOfLine = true; x = -1; continue; } startOfLine = false; if ( ch == '&' ) { result += "&"; } else if ( ch == '"' ) { result += """; } else if ( ch == '<' ) { result += "<"; } else if ( ch == '>' ) { result += ">"; } else { const int start = locator.mPos; if ( !( flags & IgnoreUrls ) ) { str = locator.getUrl(); if ( !str.isEmpty() ) { QString hyperlink; if ( str.left( 4 ) == "www." ) { hyperlink = "http://" + str; } else if ( str.left( 4 ) == "ftp." ) { hyperlink = "ftp://" + str; } else { hyperlink = str; } str = str.replace( '&', "&" ); result += "" + str + ""; x += locator.mPos - start; continue; } str = locator.getEmailAddress(); if ( !str.isEmpty() ) { // len is the length of the local part int len = str.indexOf( '@' ); QString localPart = str.left( len ); // remove the local part from the result (as '&'s have been expanded to // & we have to take care of the 4 additional characters per '&') result.truncate( result.length() - len - ( localPart.count( '&' ) * 4 ) ); x -= len; result += "" + str + ""; x += str.length() - 1; continue; } } if ( flags & HighlightText ) { str = locator.highlightedText(); if ( !str.isEmpty() ) { result += str; x += locator.mPos - start; continue; } } result += ch; } } #if KDE_IS_VERSION( 4, 0, 95 ) if ( flags & ReplaceSmileys ) { QStringList exclude; exclude << "(c)" << "(C)" << ">:-(" << ">:(" << "(B)" << "(b)" << "(P)" << "(p)"; exclude << "(O)" << "(o)" << "(D)" << "(d)" << "(E)" << "(e)" << "(K)" << "(k)"; exclude << "(I)" << "(i)" << "(L)" << "(l)" << "(8)" << "(T)" << "(t)" << "(G)"; exclude << "(g)" << "(F)" << "(f)" << "(H)"; exclude << "8)" << "(N)" << "(n)" << "(Y)" << "(y)" << "(U)" << "(u)" << "(W)" << "(w)"; static QString cachedEmoticonsThemeName; if ( cachedEmoticonsThemeName.isEmpty() ) { cachedEmoticonsThemeName = KEmoticons::currentThemeName(); } result = sEmoticons->theme( cachedEmoticonsThemeName ).parseEmoticons( result, KEmoticonsTheme::StrictParse | KEmoticonsTheme::SkipHTML, exclude ); } #endif return result; } QString LinkLocator::pngToDataUrl( const QString &iconPath ) { if ( iconPath.isEmpty() ) { return QString(); } QFile pngFile( iconPath ); if ( !pngFile.open( QIODevice::ReadOnly | QIODevice::Unbuffered ) ) { return QString(); } QByteArray ba = pngFile.readAll(); pngFile.close(); return QString::fromLatin1( "data:image/png;base64,%1" ).arg( ba.toBase64().constData() ); } QString LinkLocator::highlightedText() { // formating symbols must be prepended with a whitespace if ( ( mPos > 0 ) && !mText[mPos-1].isSpace() ) { return QString(); } const QChar ch = mText[mPos]; if ( ch != '/' && ch != '*' && ch != '_' ) { return QString(); } QRegExp re = QRegExp( QString( "\\%1([0-9A-Za-z]+)\\%2" ).arg( ch ).arg( ch ) ); if ( re.indexIn( mText, mPos ) == mPos ) { int length = re.matchedLength(); // there must be a whitespace after the closing formating symbol if ( mPos + length < mText.length() && !mText[mPos + length].isSpace() ) { return QString(); } mPos += length - 1; switch ( ch.toLatin1() ) { case '*': return "" + re.cap( 1 ) + ""; case '_': return "" + re.cap( 1 ) + ""; case '/': return "" + re.cap( 1 ) + ""; } } return QString(); } diff --git a/kpimutils/tests/testlinklocator.cpp b/kpimutils/tests/testlinklocator.cpp index 9f53eed50..da913a583 100644 --- a/kpimutils/tests/testlinklocator.cpp +++ b/kpimutils/tests/testlinklocator.cpp @@ -1,222 +1,230 @@ /* This file is part of the kpimutils library. Copyright (C) 2005 Ingo Kloecker Copyright (C) 2007 Allen Winter This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include "testlinklocator.h" #include "testlinklocator.moc" QTEST_KDEMAIN( LinkLocatorTest, NoGUI ) #include "kpimutils/linklocator.h" using namespace KPIMUtils; void LinkLocatorTest::testGetEmailAddress() { // empty input const QString emptyQString; LinkLocator ll1( emptyQString, 0 ); QVERIFY( ll1.getEmailAddress().isEmpty() ); // no '@' at scan position LinkLocator ll2( "foo@bar.baz", 0 ); QVERIFY( ll2.getEmailAddress().isEmpty() ); // '@' in local part LinkLocator ll3( "foo@bar@bar.baz", 7 ); QVERIFY( ll3.getEmailAddress().isEmpty() ); // empty local part LinkLocator ll4( "@bar.baz", 0 ); QVERIFY( ll4.getEmailAddress().isEmpty() ); LinkLocator ll5( ".@bar.baz", 1 ); QVERIFY( ll5.getEmailAddress().isEmpty() ); LinkLocator ll6( " @bar.baz", 1 ); QVERIFY( ll6.getEmailAddress().isEmpty() ); LinkLocator ll7( ".!#$%&'*+-/=?^_`{|}~@bar.baz", strlen( ".!#$%&'*+-/=?^_`{|}~" ) ); QVERIFY( ll7.getEmailAddress().isEmpty() ); // allowed special chars in local part of address LinkLocator ll8( "a.!#$%&'*+-/=?^_`{|}~@bar.baz", strlen( "a.!#$%&'*+-/=?^_`{|}~" ) ); QVERIFY( ll8.getEmailAddress() == "a.!#$%&'*+-/=?^_`{|}~@bar.baz" ); // '@' in domain part LinkLocator ll9 ( "foo@bar@bar.baz", 3 ); QVERIFY( ll9.getEmailAddress().isEmpty() ); // domain part without dot LinkLocator lla( "foo@bar", 3 ); QVERIFY( lla.getEmailAddress().isEmpty() ); LinkLocator llb( "foo@bar.", 3 ); QVERIFY( llb.getEmailAddress().isEmpty() ); LinkLocator llc( ".foo@bar", 4 ); QVERIFY( llc.getEmailAddress().isEmpty() ); LinkLocator lld( "foo@bar ", 3 ); QVERIFY( lld.getEmailAddress().isEmpty() ); LinkLocator lle( " foo@bar", 4 ); QVERIFY( lle.getEmailAddress().isEmpty() ); LinkLocator llf( "foo@bar-bar", 3 ); QVERIFY( llf.getEmailAddress().isEmpty() ); // empty domain part LinkLocator llg( "foo@", 3 ); QVERIFY( llg.getEmailAddress().isEmpty() ); LinkLocator llh( "foo@.", 3 ); QVERIFY( llh.getEmailAddress().isEmpty() ); LinkLocator lli( "foo@-", 3 ); QVERIFY( lli.getEmailAddress().isEmpty() ); // simple address LinkLocator llj( "foo@bar.baz", 3 ); QVERIFY( llj.getEmailAddress() == "foo@bar.baz" ); LinkLocator llk( "foo@bar.baz.", 3 ); QVERIFY( llk.getEmailAddress() == "foo@bar.baz" ); LinkLocator lll( ".foo@bar.baz", 4 ); QVERIFY( lll.getEmailAddress() == "foo@bar.baz" ); LinkLocator llm( "foo@bar.baz-", 3 ); QVERIFY( llm.getEmailAddress() == "foo@bar.baz" ); LinkLocator lln( "-foo@bar.baz", 4 ); QVERIFY( lln.getEmailAddress() == "foo@bar.baz" ); LinkLocator llo( "foo@bar.baz ", 3 ); QVERIFY( llo.getEmailAddress() == "foo@bar.baz" ); LinkLocator llp( " foo@bar.baz", 4 ); QVERIFY( llp.getEmailAddress() == "foo@bar.baz" ); LinkLocator llq( "foo@bar-bar.baz", 3 ); QVERIFY( llq.getEmailAddress() == "foo@bar-bar.baz" ); } void LinkLocatorTest::testGetUrl() { QStringList brackets; brackets << "" << ""; // no brackets brackets << "(" << ")"; brackets << "<" << ">"; brackets << "[" << "]"; brackets << "" << ""; for (int i = 0; i < brackets.count(); i += 2) testGetUrl2(brackets[i], brackets[i+1]); } void LinkLocatorTest::testGetUrl2(const QString &left, const QString &right) { QStringList schemas; schemas << "http://"; schemas << "https://"; schemas << "vnc://"; schemas << "fish://"; schemas << "ftp://"; schemas << "ftps://"; schemas << "sftp://"; schemas << "smb://"; schemas << "file://"; QStringList urls; urls << "www.kde.org"; urls << "user@www.kde.org"; urls << "user:pass@www.kde.org"; urls << "user:pass@www.kde.org:1234"; urls << "user:pass@www.kde.org:1234/sub/path"; urls << "user:pass@www.kde.org:1234/sub/path?a=1"; urls << "user:pass@www.kde.org:1234/sub/path?a=1#anchor"; urls << "user:pass@www.kde.org:1234/sub/path/special(123)?a=1#anchor"; urls << "user:pass@www.kde.org:1234/sub/path:with:colon/special(123)?a=1#anchor"; foreach (QString schema, schemas) { foreach (QString url, urls) { QString test(left + schema + url + right); LinkLocator ll(test, left.length()); QString gotUrl = ll.getUrl(); bool ok = ( gotUrl == (schema + url) ); - qDebug() << "check:" << (ok ? "OK" : "NOK") << test << "=>" << (schema + url); + //qDebug() << "check:" << (ok ? "OK" : "NOK") << test << "=>" << (schema + url); QVERIFY2( ok, qPrintable(test) ); } } QStringList urlsWithoutSchema; urlsWithoutSchema << ".kde.org"; urlsWithoutSchema << ".kde.org:1234/sub/path"; urlsWithoutSchema << ".kde.org:1234/sub/path?a=1"; urlsWithoutSchema << ".kde.org:1234/sub/path?a=1#anchor"; urlsWithoutSchema << ".kde.org:1234/sub/path/special(123)?a=1#anchor"; urlsWithoutSchema << ".kde.org:1234/sub/path:with:colon/special(123)?a=1#anchor"; QStringList starts; starts << "www" << "ftp" << "news:www"; foreach (QString start, starts) { foreach (QString url, urlsWithoutSchema) { QString test(left + start + url + right); LinkLocator ll(test, left.length()); QString gotUrl = ll.getUrl(); bool ok = ( gotUrl == (start + url) ); - qDebug() << "check:" << (ok ? "OK" : "NOK") << test << "=>" << (start + url); + //qDebug() << "check:" << (ok ? "OK" : "NOK") << test << "=>" << (start + url); QVERIFY2( ok, qPrintable(test) ); } } // mailto { QString addr = "mailto:test@kde.org"; QString test(left + addr + right); LinkLocator ll(test, left.length()); QString gotUrl = ll.getUrl(); bool ok = ( gotUrl == addr ); - qDebug() << "check:" << (ok ? "OK" : "NOK") << test << "=>" << addr; + //qDebug() << "check:" << (ok ? "OK" : "NOK") << test << "=>" << addr; QVERIFY2( ok, qPrintable(test) ); } } void LinkLocatorTest::testHtmlConvert_data() { QTest::addColumn("plainText"); QTest::addColumn("flags"); QTest::addColumn("htmlText"); - QTest::newRow( "" ) << "foo" << 0 << "foo"; - QTest::newRow( "" ) << " foo " << 0 << " foo "; + //QTest::newRow( "" ) << "foo" << 0 << "foo"; + //QTest::newRow( "" ) << " foo " << 0 << " foo "; // Linker error when using PreserveSpaces, therefore the hardcoded 0x01 QTest::newRow( "" ) << " foo" << 0x01 << " foo"; QTest::newRow( "" ) << " foo" << 0x01 << "  foo"; QTest::newRow( "" ) << " foo " << 0x01 << "  foo  "; + QTest::newRow( "" ) << " foo " << 0x01 << "  foo "; + QTest::newRow( "" ) << "bla bla bla bla bla" << 0x01 << "bla bla bla bla bla"; + QTest::newRow( "" ) << "bla bla bla \n bla bla bla " << 0x01 + << "bla bla bla 
\n  bla bla bla "; + QTest::newRow( "" ) << "bla bla bla" << 0x01 + << "bla bla  bla"; + QTest::newRow( "" ) << " bla bla \n bla bla a\n bla bla " << 0x01 + << " bla bla 
\n bla bla a
\n  bla bla "; } void LinkLocatorTest::testHtmlConvert() { QFETCH(QString, plainText); QFETCH(int, flags); QFETCH(QString, htmlText); QString actualHtml = LinkLocator::convertToHtml( plainText, flags ); QCOMPARE( actualHtml, htmlText ); }